diff --git a/.gitattributes b/.gitattributes
index 8ad74f78d..052d0ce75 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,5 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
+
+# Remove JS from the language graph: It's not a language the repository truly uses.
+*.js linguist-vendored
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..b1acc4826
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: MewPurPur
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..a05e3b8e4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,10 @@
+---
+name: Bug report
+about: Report a bug in GodSVG
+title: Title
+labels: bug
+assignees: ''
+
+---
+
+Give a clear and concise description of what the bug is and what the expected behavior is. For non-trivial issues, provide reproduction steps and screenshots if relevant.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..3191b285b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,10 @@
+---
+name: Feature request
+about: Suggest a new feature for GodSVG
+title: Title
+labels: proposal
+assignees: ''
+
+---
+
+Describe the limitation you're facing and how the feature will help resolve it. If you can, give more detail about the implementation.
diff --git a/.gitignore b/.gitignore
index 470918367..d1ac430c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,7 @@
# Godot 4+ specific ignores
.godot/
+.DS_Store
+
+# Imported translations (automatically generated from CSV files)
+*.translation
+.~lock.*
diff --git a/AppInfo.gd b/AppInfo.gd
new file mode 100644
index 000000000..81954e9d2
--- /dev/null
+++ b/AppInfo.gd
@@ -0,0 +1,27 @@
+## Stores basic information about GodSVG.
+class_name AppInfo extends RefCounted
+
+const project_founder_and_manager: Array[String] = ["MewPurPur"]
+
+
+# The developers who have contributed significant patches to the MIT-licensed source code
+# of GodSVG, as well as people who have contributed other things like translations and art
+# to the repository, are listed here. What counts as significant is arbitrarily decided.
+# Entries are formatted as follows:
+# Option 1: Real Name (GitHub username)
+# Option 2: GitHub username
+
+const authors: Array[String] = [
+ "ajreckof",
+ "aladvs",
+ "Alex2782",
+ "DevPoodle",
+ "ilikefrogs101",
+ "Kiisu-Master",
+ "MewPurPur",
+ "Serem Titus (SeremTitus)",
+ "Swarkin",
+ "thiagola92",
+ "Tom Blackwell (Volts-s)",
+ "volkov"
+]
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..be8d61caa
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,41 @@
+## Governance
+
+Your contribution is always appreciated!
+
+Contributions don't need to be perfect, but they must move GodSVG in the right direction. If you are planning to implement a feature or overhaul a system, it's important to write a proposal and discuss your ideas. I will try to quickly accept or decline them. Please do understand that PRs with a large maintenance cost may be under high scrutiny because of their long-term responsibility, even in the absence of the original contributor.
+
+## Setup
+
+GodSVG is made in Godot using its GDScript language. Refer to the [README](https://github.com/MewPurPur/GodSVG?tab=readme-ov-file#how-to-get-it) on how to get GodSVG running.
+
+Git must be configured, then you can clone the repository to your local machine: `git clone git@github.com:MewPurPur/GodSVG.git`
+
+The documentation won't go into detail about how to use Git. Refer to outside resources if you are unfamiliar with it.
+
+## PR workflow
+
+Look through the list of issues to see if your contribution would resolve any of them. If said issue is not assigned to anyone and you don't want anyone else to work on it, ask to be assigned to the issue. If an issue doesn't exist and you want to fix a bug, then it's a good practice, but not required, to make an issue for it.
+
+1. Fork the repository.
+2. Create a new branch: `git checkout -b implement-gradients`
+3. Make your modifications, add them with `git add .`
+4. Commit your changes: `git commit -m "Implement linear gradients"`
+5. Push to the branch: `git push origin implement-gradients`
+6. Create a new pull request with a clear and informative title and describe your changes.
+
+This is the preferred workflow, but tidiness is not as important as work being done, so feel free to do something different you may be comfortable with.
+
+After submitting your pull request, I (MewPurPur) will review your changes and may provide feedback or request modifications. Be responsive to any comments or suggestions. Once your pull request is approved, it will be merged. Afterward, you can delete your branch from your fork.
+
+## Code style
+
+For scripts, only GDScript code is allowed. Follow the [GDScript style guide](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html). Most of its rules are enforced here. Additionally:
+
+- Static typing is predominantly used.
+- Comments are normally written like sentences with punctuation.
+- Two spaces are used to separate code and inline comments.
+- For empty lines in the middle of indented blocks, the scope's indentation is kept.
+- Class names use `class_name X extends Y` syntax.
+- `@export` for nodes is only used if the runtime structure is not known.
+
+Don't make pull requests for code style changes without discussing them first (unless it's for corrections to abide by the ones described here). Pull requests may also get production tweaks to fix their style before being merged.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..f8ed7fc7b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2023 MewPurPur
+Copyright (c) 2023-present GodSVG contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 746b62dfb..ec93ec6e1 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,58 @@
# GodSVG
-GodSVG is an application in extremely early development built with Godot for creating optimal Scalable Vector Graphics (SVG) files. It is specifically designed for programmers, allowing them to easily edit individual SVG elements and view the corresponding code in real-time.
-GodSVG is inspired by the need for an editor for programmers that produces optimized SVG without using unnecessary attributes or metadata.
+
+
+
+
+**GodSVG is an editor for Scalable Vector Graphics (SVG) files.** Unlike other editors, it represents the SVG code directly, doesn't add any metadata, and even lets you edit the SVG code in real time. GodSVG is inspired by the need for an SVG editor without abstractions that produces clean and optimized SVG files.
## Features
-- Interactive SVG editing: Modify individual elements of an SVG file using a user-friendly interface.
-- Real-time code preview: Instantly view the SVG code as you make changes to the elements.
-- Optimized SVG output: Generate clean and efficient SVG files.
+- **Interactive SVG editing:** Modify individual elements of an SVG file using a user-friendly interface.
+- **Real-time code:** As you manipulate elements in the UI, code is instantly generated and can be edited.
+- **Optimized SVG output:** Generate clean and efficient SVG files.
+- **Accessible on mobile:** GodSVG aims to be usable on mobile devices.
+
+
+
+## How to get it
+
+Download the version you want from [the list of GodSVG releases](https://github.com/MewPurPur/GodSVG/releases).
+
+Note that if you're on MacOS, you need to [disable Gatekeeper](https://disable-gatekeeper.github.io/) if you haven't yet. I don't have the time or money to deal with Apple's gatekeeping.
+
+Link to the web build: https://mewpurpur.github.io/GodSVG/web-build (Currently experimental)
+
+To run the latest unreleased version, you can download Godot from https://godotengine.org (v4.2 minimum). After getting the repository files on your machine, you must open Godot, click the "Import" button, and import the `project.godot` folder. If there are a lot of errors which some people have reported, it's a Godot bug. Try closing and opening the project a few times, changing small things on the code that errors out, etc. until the errors hopefully clear.
-## Installation
+## How to use it
-Currently, there are no pre-built binaries available for GodSVG. However, you can still run it by following these steps:
+Documentation for GodSVG is likely eventually going to be built-in. In the meantime, the basics of using it will be outlined here. This documentation is for the current master, which is a little ahead of the alpha 2 release.
-1. Clone the repository: `git clone https://github.com/mew-pur-pur/GodSVG.git`
-2. Open the project in the Godot Engine.
-3. Build and run the project within the Godot Engine editor.
+If you want to import an existing graphic from scratch, use the Import button on top of the code editor or drag-and-drop an SVG file into the app.
-## Contributing
+To add new shapes, press the "+ Add new tag" button or right-click inside the viewport, then select your shape from the dropdown. After your shape is added, you can drag its handles in the viewport to change its shape, or modify the attributes in the inspector to change its other attributes, like fill and stroke. You can also always modify the SVG code directly.
-Contributions to GodSVG are very welcome! To do so, do the following:
+In the inspector, you can hover each tag's fields to see which attribute they represent. You may select tags in the viewport on the right or the inspector on the left, and right-click to do operations on them, like deleting them (can be done with the Delete key) moving them up or down (can also be done with Ctrl+Up and Ctrl+Down), duplicating them (can also be done with Ctrl+D), or moving them within the inspector by drag-and-dropping.
-1. Fork the repository.
-2. Create a new branch: `git checkout -b implement-gradients`
-3. Make your modifications.
-4. Commit your changes: `git commit -m "Implement linear gradients"`
-5. Push to the branch: `git push origin implement-gradients`
-6. Create a new pull request and describe your changes in detail.
+Pathdata attributes have a very complex editor that allows for selecting individual path commands with a lot of similarities to tags. You can right-click the path command and click "Insert After", then pick the one you want. If you're used to SVG paths, you can also use the M, L, H, V, Z, A, Q, T, C, S keys to insert a new path command after a selected one; pressing Shift will also make the new command absolute instead of relative.
-Since the app is in early development, tidiness is not of utmost importance, so feel free to use a different PR workflow (but the above is still preferable).
-To report bugs, use Github's issue form. If features are proposed, please provide a mock-up UI or implementation details.
+Multiple tagsĀ or path commands can be selected as usual with Ctrl+Click and Shift+Click. Additionally, double-clicking a path command will select the whole subpath it's in.
+
+To export the graphic, use the Export button on top of the code editor.
+
+## Community and contributing
+
+Contributions are very welcome! GodSVG is built in Godot. For code contributions, read [Contributing Guidelines](CONTRIBUTING.md). Before starting work on features, first propose them by using the issue form and wait for approval.
+
+To report bugs or propose features, use Github's issue form. For more casual discussion around the tool or contributing to it, find me on [GodSVG's Discord](https://discord.gg/R8pM6vXWTY).
## License
-GodSVG is licensed under the MIT License.
+GodSVG is licensed under the MIT License:
+
+- You are free to use GodSVG for any purpose. GodSVG's license terms and copyright do not apply to the content created with it.
+- You can study how GodSVG works and change it.
+- You may distribute modified versions of GodSVG. Derivative products may use a different license, but they must still document that they derive from the MIT-licensed GodSVG.
+
+The above explanation reflects my understanding of my own license terms and does not constitute legal advice.
diff --git a/export_presets.cfg b/export_presets.cfg
new file mode 100644
index 000000000..287617c5d
--- /dev/null
+++ b/export_presets.cfg
@@ -0,0 +1,448 @@
+[preset.0]
+
+name="Windows Desktop"
+platform="Windows Desktop"
+runnable=true
+dedicated_server=false
+custom_features=""
+export_filter="all_resources"
+include_filter=""
+exclude_filter="web-build/*, *.md"
+export_path="../GodSVG meta/GodSVG exports/GodSVG_windows.exe"
+encryption_include_filters=""
+encryption_exclude_filters=""
+encrypt_pck=false
+encrypt_directory=false
+
+[preset.0.options]
+
+custom_template/debug=""
+custom_template/release="/home/volter/Desktop/godot/bin/godot.windows.template_release.x86_64.exe"
+debug/export_console_wrapper=1
+binary_format/embed_pck=true
+texture_format/bptc=true
+texture_format/s3tc=true
+texture_format/etc=false
+texture_format/etc2=false
+binary_format/architecture="x86_64"
+codesign/enable=false
+codesign/timestamp=true
+codesign/timestamp_server_url=""
+codesign/digest_algorithm=1
+codesign/description=""
+codesign/custom_options=PackedStringArray()
+application/modify_resources=true
+application/icon=""
+application/console_wrapper_icon=""
+application/icon_interpolation=4
+application/file_version=""
+application/product_version=""
+application/company_name="GodSVG contributors"
+application/product_name="GodSVG"
+application/file_description="GodSVG"
+application/copyright="2023-present GodSVG contributors"
+application/trademarks=""
+application/export_angle=0
+ssh_remote_deploy/enabled=false
+ssh_remote_deploy/host="user@host_ip"
+ssh_remote_deploy/port="22"
+ssh_remote_deploy/extra_args_ssh=""
+ssh_remote_deploy/extra_args_scp=""
+ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
+$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
+$trigger = New-ScheduledTaskTrigger -Once -At 00:00
+$settings = New-ScheduledTaskSettingsSet
+$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
+Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
+Start-ScheduledTask -TaskName godot_remote_debug
+while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
+Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
+ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
+Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
+Remove-Item -Recurse -Force '{temp_dir}'"
+
+[preset.1]
+
+name="Linux/X11"
+platform="Linux/X11"
+runnable=true
+dedicated_server=false
+custom_features=""
+export_filter="all_resources"
+include_filter=""
+exclude_filter="web-build/*, *.md, *.ico"
+export_path="../GodSVG meta/GodSVG exports/GodSVG_linuxbsd.x86_64"
+encryption_include_filters=""
+encryption_exclude_filters=""
+encrypt_pck=false
+encrypt_directory=false
+
+[preset.1.options]
+
+custom_template/debug=""
+custom_template/release="/home/volter/Desktop/godot/bin/godot.linuxbsd.template_release.x86_64"
+debug/export_console_wrapper=1
+binary_format/embed_pck=true
+texture_format/bptc=true
+texture_format/s3tc=true
+texture_format/etc=false
+texture_format/etc2=false
+binary_format/architecture="x86_64"
+ssh_remote_deploy/enabled=false
+ssh_remote_deploy/host="user@host_ip"
+ssh_remote_deploy/port="22"
+ssh_remote_deploy/extra_args_ssh=""
+ssh_remote_deploy/extra_args_scp=""
+ssh_remote_deploy/run_script="#!/usr/bin/env bash
+export DISPLAY=:0
+unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
+\"{temp_dir}/{exe_name}\" {cmd_args}"
+ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
+kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
+rm -rf \"{temp_dir}\""
+
+[preset.2]
+
+name="macOS"
+platform="macOS"
+runnable=true
+dedicated_server=false
+custom_features=""
+export_filter="all_resources"
+include_filter=""
+exclude_filter="web-build/*, *.md, *.ico"
+export_path="../GodSVG meta/GodSVG exports/GodSVG_macos.zip"
+encryption_include_filters=""
+encryption_exclude_filters=""
+encrypt_pck=false
+encrypt_directory=false
+
+[preset.2.options]
+
+export/distribution_type=1
+binary_format/architecture="universal"
+custom_template/debug=""
+custom_template/release="/home/volter/Desktop/godot/bin/macos.zip"
+debug/export_console_wrapper=1
+application/icon=""
+application/icon_interpolation=4
+application/bundle_identifier="com.godsvg"
+application/signature=""
+application/app_category="Graphics-design"
+application/short_version=""
+application/version=""
+application/copyright="2023-present GodSVG contributors"
+application/copyright_localized={}
+application/min_macos_version="10.12"
+application/export_angle=0
+display/high_res=true
+xcode/platform_build="14C18"
+xcode/sdk_version="13.1"
+xcode/sdk_build="22C55"
+xcode/sdk_name="macosx13.1"
+xcode/xcode_version="1420"
+xcode/xcode_build="14C18"
+codesign/codesign=1
+codesign/installer_identity=""
+codesign/apple_team_id=""
+codesign/identity=""
+codesign/entitlements/custom_file=""
+codesign/entitlements/allow_jit_code_execution=false
+codesign/entitlements/allow_unsigned_executable_memory=false
+codesign/entitlements/allow_dyld_environment_variables=false
+codesign/entitlements/disable_library_validation=false
+codesign/entitlements/audio_input=false
+codesign/entitlements/camera=false
+codesign/entitlements/location=false
+codesign/entitlements/address_book=false
+codesign/entitlements/calendars=false
+codesign/entitlements/photos_library=false
+codesign/entitlements/apple_events=false
+codesign/entitlements/debugging=false
+codesign/entitlements/app_sandbox/enabled=false
+codesign/entitlements/app_sandbox/network_server=false
+codesign/entitlements/app_sandbox/network_client=false
+codesign/entitlements/app_sandbox/device_usb=false
+codesign/entitlements/app_sandbox/device_bluetooth=false
+codesign/entitlements/app_sandbox/files_downloads=0
+codesign/entitlements/app_sandbox/files_pictures=0
+codesign/entitlements/app_sandbox/files_music=0
+codesign/entitlements/app_sandbox/files_movies=0
+codesign/entitlements/app_sandbox/files_user_selected=0
+codesign/entitlements/app_sandbox/helper_executables=[]
+codesign/custom_options=PackedStringArray()
+notarization/notarization=0
+privacy/microphone_usage_description=""
+privacy/microphone_usage_description_localized={}
+privacy/camera_usage_description=""
+privacy/camera_usage_description_localized={}
+privacy/location_usage_description=""
+privacy/location_usage_description_localized={}
+privacy/address_book_usage_description=""
+privacy/address_book_usage_description_localized={}
+privacy/calendar_usage_description=""
+privacy/calendar_usage_description_localized={}
+privacy/photos_library_usage_description=""
+privacy/photos_library_usage_description_localized={}
+privacy/desktop_folder_usage_description=""
+privacy/desktop_folder_usage_description_localized={}
+privacy/documents_folder_usage_description=""
+privacy/documents_folder_usage_description_localized={}
+privacy/downloads_folder_usage_description=""
+privacy/downloads_folder_usage_description_localized={}
+privacy/network_volumes_usage_description=""
+privacy/network_volumes_usage_description_localized={}
+privacy/removable_volumes_usage_description=""
+privacy/removable_volumes_usage_description_localized={}
+ssh_remote_deploy/enabled=false
+ssh_remote_deploy/host="user@host_ip"
+ssh_remote_deploy/port="22"
+ssh_remote_deploy/extra_args_ssh=""
+ssh_remote_deploy/extra_args_scp=""
+ssh_remote_deploy/run_script="#!/usr/bin/env bash
+unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
+open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}"
+ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
+kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")
+rm -rf \"{temp_dir}\""
+
+[preset.3]
+
+name="Web"
+platform="Web"
+runnable=true
+dedicated_server=false
+custom_features=""
+export_filter="exclude"
+export_files=PackedStringArray()
+include_filter=""
+exclude_filter="web-build/*, *.md, *.ico"
+export_path="../GodSVG meta/GodSVG exports/GodSVG.html"
+encryption_include_filters=""
+encryption_exclude_filters=""
+encrypt_pck=false
+encrypt_directory=false
+
+[preset.3.options]
+
+custom_template/debug=""
+custom_template/release="/home/volter/Desktop/godot/bin/web_release.zip"
+variant/extensions_support=false
+vram_texture_compression/for_desktop=true
+vram_texture_compression/for_mobile=false
+html/export_icon=true
+html/custom_html_shell=""
+html/head_include=""
+html/canvas_resize_policy=2
+html/focus_canvas_on_start=true
+html/experimental_virtual_keyboard=false
+progressive_web_app/enabled=false
+progressive_web_app/offline_page=""
+progressive_web_app/display=1
+progressive_web_app/orientation=0
+progressive_web_app/icon_144x144=""
+progressive_web_app/icon_180x180=""
+progressive_web_app/icon_512x512=""
+progressive_web_app/background_color=Color(0, 0, 0, 1)
+
+[preset.4]
+
+name="Android"
+platform="Android"
+runnable=true
+dedicated_server=false
+custom_features=""
+export_filter="all_resources"
+include_filter=""
+exclude_filter=""
+export_path=""
+encryption_include_filters=""
+encryption_exclude_filters=""
+encrypt_pck=false
+encrypt_directory=false
+
+[preset.4.options]
+
+custom_template/debug=""
+custom_template/release=""
+gradle_build/use_gradle_build=false
+gradle_build/export_format=0
+gradle_build/min_sdk=""
+gradle_build/target_sdk=""
+architectures/armeabi-v7a=false
+architectures/arm64-v8a=true
+architectures/x86=false
+architectures/x86_64=false
+version/code=1
+version/name=""
+package/unique_name="com.example.$genname"
+package/name=""
+package/signed=true
+package/app_category=2
+package/retain_data_on_uninstall=false
+package/exclude_from_recents=false
+package/show_in_android_tv=false
+package/show_in_app_library=true
+package/show_as_launcher_app=false
+launcher_icons/main_192x192=""
+launcher_icons/adaptive_foreground_432x432=""
+launcher_icons/adaptive_background_432x432=""
+graphics/opengl_debug=false
+xr_features/xr_mode=0
+screen/immersive_mode=true
+screen/support_small=true
+screen/support_normal=true
+screen/support_large=true
+screen/support_xlarge=true
+user_data_backup/allow=false
+command_line/extra_args=""
+apk_expansion/enable=false
+apk_expansion/SALT=""
+apk_expansion/public_key=""
+permissions/custom_permissions=PackedStringArray()
+permissions/access_checkin_properties=false
+permissions/access_coarse_location=false
+permissions/access_fine_location=false
+permissions/access_location_extra_commands=false
+permissions/access_mock_location=false
+permissions/access_network_state=false
+permissions/access_surface_flinger=false
+permissions/access_wifi_state=false
+permissions/account_manager=false
+permissions/add_voicemail=false
+permissions/authenticate_accounts=false
+permissions/battery_stats=false
+permissions/bind_accessibility_service=false
+permissions/bind_appwidget=false
+permissions/bind_device_admin=false
+permissions/bind_input_method=false
+permissions/bind_nfc_service=false
+permissions/bind_notification_listener_service=false
+permissions/bind_print_service=false
+permissions/bind_remoteviews=false
+permissions/bind_text_service=false
+permissions/bind_vpn_service=false
+permissions/bind_wallpaper=false
+permissions/bluetooth=false
+permissions/bluetooth_admin=false
+permissions/bluetooth_privileged=false
+permissions/brick=false
+permissions/broadcast_package_removed=false
+permissions/broadcast_sms=false
+permissions/broadcast_sticky=false
+permissions/broadcast_wap_push=false
+permissions/call_phone=false
+permissions/call_privileged=false
+permissions/camera=false
+permissions/capture_audio_output=false
+permissions/capture_secure_video_output=false
+permissions/capture_video_output=false
+permissions/change_component_enabled_state=false
+permissions/change_configuration=false
+permissions/change_network_state=false
+permissions/change_wifi_multicast_state=false
+permissions/change_wifi_state=false
+permissions/clear_app_cache=false
+permissions/clear_app_user_data=false
+permissions/control_location_updates=false
+permissions/delete_cache_files=false
+permissions/delete_packages=false
+permissions/device_power=false
+permissions/diagnostic=false
+permissions/disable_keyguard=false
+permissions/dump=false
+permissions/expand_status_bar=false
+permissions/factory_test=false
+permissions/flashlight=false
+permissions/force_back=false
+permissions/get_accounts=false
+permissions/get_package_size=false
+permissions/get_tasks=false
+permissions/get_top_activity_info=false
+permissions/global_search=false
+permissions/hardware_test=false
+permissions/inject_events=false
+permissions/install_location_provider=false
+permissions/install_packages=false
+permissions/install_shortcut=false
+permissions/internal_system_window=false
+permissions/internet=false
+permissions/kill_background_processes=false
+permissions/location_hardware=false
+permissions/manage_accounts=false
+permissions/manage_app_tokens=false
+permissions/manage_documents=false
+permissions/manage_external_storage=false
+permissions/master_clear=false
+permissions/media_content_control=false
+permissions/modify_audio_settings=false
+permissions/modify_phone_state=false
+permissions/mount_format_filesystems=false
+permissions/mount_unmount_filesystems=false
+permissions/nfc=false
+permissions/persistent_activity=false
+permissions/process_outgoing_calls=false
+permissions/read_calendar=false
+permissions/read_call_log=false
+permissions/read_contacts=false
+permissions/read_external_storage=false
+permissions/read_frame_buffer=false
+permissions/read_history_bookmarks=false
+permissions/read_input_state=false
+permissions/read_logs=false
+permissions/read_phone_state=false
+permissions/read_profile=false
+permissions/read_sms=false
+permissions/read_social_stream=false
+permissions/read_sync_settings=false
+permissions/read_sync_stats=false
+permissions/read_user_dictionary=false
+permissions/reboot=false
+permissions/receive_boot_completed=false
+permissions/receive_mms=false
+permissions/receive_sms=false
+permissions/receive_wap_push=false
+permissions/record_audio=false
+permissions/reorder_tasks=false
+permissions/restart_packages=false
+permissions/send_respond_via_message=false
+permissions/send_sms=false
+permissions/set_activity_watcher=false
+permissions/set_alarm=false
+permissions/set_always_finish=false
+permissions/set_animation_scale=false
+permissions/set_debug_app=false
+permissions/set_orientation=false
+permissions/set_pointer_speed=false
+permissions/set_preferred_applications=false
+permissions/set_process_limit=false
+permissions/set_time=false
+permissions/set_time_zone=false
+permissions/set_wallpaper=false
+permissions/set_wallpaper_hints=false
+permissions/signal_persistent_processes=false
+permissions/status_bar=false
+permissions/subscribed_feeds_read=false
+permissions/subscribed_feeds_write=false
+permissions/system_alert_window=false
+permissions/transmit_ir=false
+permissions/uninstall_shortcut=false
+permissions/update_device_stats=false
+permissions/use_credentials=false
+permissions/use_sip=false
+permissions/vibrate=false
+permissions/wake_lock=false
+permissions/write_apn_settings=false
+permissions/write_calendar=false
+permissions/write_call_log=false
+permissions/write_contacts=false
+permissions/write_external_storage=false
+permissions/write_gservices=false
+permissions/write_history_bookmarks=false
+permissions/write_profile=false
+permissions/write_secure_settings=false
+permissions/write_settings=false
+permissions/write_sms=false
+permissions/write_social_stream=false
+permissions/write_sync_settings=false
+permissions/write_user_dictionary=false
diff --git a/icon.png b/icon.png
deleted file mode 100644
index a89fa184e..000000000
Binary files a/icon.png and /dev/null differ
diff --git a/project.godot b/project.godot
index 3046e317e..d6113f1d4 100644
--- a/project.godot
+++ b/project.godot
@@ -11,29 +11,214 @@ config_version=5
[application]
config/name="GodSVG"
+config/version="v1.0-alpha2"
config/tags=PackedStringArray("project")
-run/main_scene="res://src/interface_elements/main_scene.tscn"
-config/features=PackedStringArray("4.1", "Forward Plus")
-config/icon="res://icon.png"
+run/main_scene="res://src/ui_parts/main_scene.tscn"
+config/use_custom_user_dir=true
+config/features=PackedStringArray("4.2", "Forward Plus")
+run/low_processor_mode=true
+boot_splash/bg_color=Color(0.1065, 0.1181, 0.15, 1)
+boot_splash/image="res://visual/splash.png"
+boot_splash/fullsize=false
+config/icon="res://visual/icon.png"
+config/windows_native_icon="res://visual/icon.ico"
+
+[audio]
+
+driver/driver="Dummy"
[autoload]
+GlobalSettings="*res://src/GlobalSettings.gd"
SVG="*res://src/SVG.gd"
+Indications="*res://src/Indications.gd"
+HandlerGUI="*res://src/HandlerGUI.gd"
[display]
window/size/viewport_width=1024
window/size/viewport_height=640
window/size/mode=2
+window/energy_saving/keep_screen_on=false
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
mouse_cursor/tooltip_position_offset=Vector2(0, 10)
+[filesystem]
+
+import/blender/enabled=false
+import/fbx/enabled=false
+
[gui]
theme/custom="res://visual/main_theme.tres"
timers/tooltip_delay_sec=0.4
+[input]
+
+import={
+"deadzone": 0.5,
+"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":0,"physical_keycode":79,"key_label":0,"unicode":111,"echo":false,"script":null)
+, 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":0,"physical_keycode":73,"key_label":0,"unicode":105,"echo":false,"script":null)
+]
+}
+export={
+"deadzone": 0.5,
+"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":0,"physical_keycode":69,"key_label":0,"unicode":101,"echo":false,"script":null)
+]
+}
+delete={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194312,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+move_up={
+"deadzone": 0.5,
+"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":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+move_down={
+"deadzone": 0.5,
+"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":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+undo={
+"deadzone": 0.5,
+"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":0,"physical_keycode":90,"key_label":0,"unicode":122,"echo":false,"script":null)
+]
+}
+redo={
+"deadzone": 0.5,
+"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":true,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":90,"echo":false,"script":null)
+, 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":0,"physical_keycode":89,"key_label":0,"unicode":121,"echo":false,"script":null)
+]
+}
+duplicate={
+"deadzone": 0.5,
+"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":0,"physical_keycode":68,"key_label":0,"unicode":100,"echo":false,"script":null)
+]
+}
+select_all={
+"deadzone": 0.5,
+"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":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null)
+]
+}
+move_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":77,"key_label":0,"unicode":109,"echo":false,"script":null)
+]
+}
+move_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":77,"key_label":0,"unicode":77,"echo":false,"script":null)
+]
+}
+line_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":76,"key_label":0,"unicode":108,"echo":false,"script":null)
+]
+}
+line_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":76,"key_label":0,"unicode":76,"echo":false,"script":null)
+]
+}
+horizontal_line_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":104,"echo":false,"script":null)
+]
+}
+horizontal_line_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":72,"echo":false,"script":null)
+]
+}
+vertical_line_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":86,"key_label":0,"unicode":118,"echo":false,"script":null)
+]
+}
+vertical_line_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":86,"key_label":0,"unicode":86,"echo":false,"script":null)
+]
+}
+close_path_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":122,"echo":false,"script":null)
+]
+}
+close_path_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":90,"echo":false,"script":null)
+]
+}
+elliptical_arc_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null)
+]
+}
+elliptical_arc_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":65,"echo":false,"script":null)
+]
+}
+quadratic_bezier_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":113,"echo":false,"script":null)
+]
+}
+quadratic_bezier_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":81,"echo":false,"script":null)
+]
+}
+shorthand_quadratic_bezier_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":84,"key_label":0,"unicode":116,"echo":false,"script":null)
+]
+}
+shorthand_quadratic_bezier_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":84,"key_label":0,"unicode":84,"echo":false,"script":null)
+]
+}
+cubic_bezier_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"echo":false,"script":null)
+]
+}
+cubic_bezier_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":67,"echo":false,"script":null)
+]
+}
+shorthand_cubic_bezier_relative={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null)
+]
+}
+shorthand_cubic_bezier_absolute={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":83,"echo":false,"script":null)
+]
+}
+save={
+"deadzone": 0.5,
+"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":0,"physical_keycode":83,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+
+[input_devices]
+
+pointing/android/enable_pan_and_scale_gestures=true
+
+[internationalization]
+
+locale/translations=PackedStringArray("res://translations/translation_sheet.bg.translation", "res://translations/translation_sheet.en.translation", "res://translations/translation_sheet.de.translation", "res://translations/translation_sheet.ru_RU.translation", "res://translations/translation_sheet.uk_UA.translation")
+
[rendering]
-environment/defaults/default_clear_color=Color(0.12, 0.138667, 0.2, 1)
+renderer/rendering_method="gl_compatibility"
+textures/vram_compression/import_etc2_astc=true
+environment/defaults/default_clear_color=Color(0.12, 0.132, 0.2, 1)
diff --git a/src/GlobalSettings.gd b/src/GlobalSettings.gd
new file mode 100644
index 000000000..e0005b2aa
--- /dev/null
+++ b/src/GlobalSettings.gd
@@ -0,0 +1,142 @@
+## This singleton handles save data and settings.
+extends Node
+
+var save_data := SaveData.new()
+const save_path = "user://save.tres"
+
+var _palettes := SavedColorPalettes.new()
+const palettes_save_path = "user://palettes.tres"
+
+const config_path = "user://config.tres"
+var config := ConfigFile.new()
+
+# Don't have the language setting here, so it's not reset.
+const default_config = {
+ "input": {
+ "invert_zoom": false,
+ "wrap_mouse": false,
+ "use_ctrl_for_zoom": true,
+ },
+ "autoformat": {
+ "xml_add_trailing_newline": false,
+ "xml_shorthand_tags": true,
+ "number_enable_autoformatting": false,
+ "number_remove_zero_padding": false,
+ "number_remove_leading_zero": true,
+ "number_remove_plus_sign": false,
+ "color_enable_autoformatting": false,
+ "color_convert_rgb_to_hex": false,
+ "color_convert_named_to_hex": true,
+ "color_use_shorthand_hex_code": true,
+ "color_use_short_named_colors": false,
+ "path_enable_autoformatting": false,
+ "path_compress_numbers": true,
+ "path_minimize_spacing": true,
+ "path_remove_spacing_after_flags": false,
+ "path_remove_consecutive_commands": true,
+ "transform_enable_autoformatting": false,
+ "transform_compress_numbers": true,
+ "transform_minimize_spacing": true,
+ "transform_remove_unnecessary_params": true,
+ },
+}
+
+var language: StringName:
+ set(new_value):
+ language = new_value
+ TranslationServer.set_locale(new_value)
+ save_setting("text", "language")
+
+# Input
+var invert_zoom := false
+var wrap_mouse := false
+var use_ctrl_for_zoom := true
+
+# Autoformat
+var xml_add_trailing_newline := false
+var xml_shorthand_tags := true
+var number_enable_autoformatting := false
+var number_remove_zero_padding := true
+var number_remove_leading_zero := false
+var color_enable_autoformatting := false
+var color_convert_rgb_to_hex := false
+var color_convert_named_to_hex := true
+var color_use_shorthand_hex_code := true
+var color_use_short_named_colors := false
+var path_enable_autoformatting := false
+var path_compress_numbers := true
+var path_minimize_spacing := true
+var path_remove_spacing_after_flags := false
+var path_remove_consecutive_commands := true
+var transform_enable_autoformatting := false
+var transform_compress_numbers := true
+var transform_minimize_spacing := true
+var transform_remove_unnecessary_params := true
+
+
+func toggle_bool_setting(section: String, setting: String) -> void:
+ set(setting, !get(setting))
+ save_setting(section, setting)
+
+func save_setting(section: String, setting: String) -> void:
+ config.set_value(section, setting, get(setting))
+ config.save(config_path)
+
+func modify_save_data(property: StringName, new_value: Variant) -> void:
+ save_data.set(property, new_value)
+ ResourceSaver.save(save_data, save_path)
+
+func save_user_data() -> void:
+ ResourceSaver.save(save_data, save_path)
+ ResourceSaver.save(_palettes, palettes_save_path)
+
+func load_user_data() -> void:
+ if FileAccess.file_exists(save_path):
+ save_data = ResourceLoader.load(save_path)
+
+ if FileAccess.file_exists(palettes_save_path):
+ _palettes = ResourceLoader.load(palettes_save_path)
+ else:
+ var default_palette_pure := ColorPalette.new("Pure", [
+ NamedColor.new("fff", "White"),
+ NamedColor.new("000", "Black"),
+ NamedColor.new("f00", "Red"),
+ NamedColor.new("0f0", "Green"),
+ NamedColor.new("00f", "Blue"),
+ NamedColor.new("ff0", "Yellow"),
+ NamedColor.new("f0f", "Magenta"),
+ NamedColor.new("0ff", "Cyan"),
+ ])
+ get_palettes().append(default_palette_pure)
+ ResourceSaver.save(_palettes, palettes_save_path)
+
+func _exit_tree() -> void:
+ save_data.window_mode = DisplayServer.window_get_mode()
+ save_user_data()
+
+func _enter_tree() -> void:
+ load_settings()
+ load_user_data()
+ DisplayServer.window_set_mode(save_data.window_mode)
+ get_window().wrap_controls = true # Prevents the main window from getting too small.
+
+
+func load_settings() -> void:
+ var error := config.load(config_path)
+ if error:
+ reset_settings()
+ language = &"en"
+ else:
+ for section in config.get_sections():
+ for setting in config.get_section_keys(section):
+ set(setting, config.get_value(section, setting))
+ save_setting(section, setting)
+
+func reset_settings() -> void:
+ for section in default_config.keys():
+ for setting in default_config[section].keys():
+ set(setting, default_config[section][setting])
+ save_setting(section, setting)
+
+func get_palettes() -> Array[ColorPalette]:
+ return _palettes.palettes
diff --git a/src/HandlerGUI.gd b/src/HandlerGUI.gd
new file mode 100644
index 000000000..83e73cb13
--- /dev/null
+++ b/src/HandlerGUI.gd
@@ -0,0 +1,183 @@
+extends Node
+
+signal _in_focus
+
+const ImportWarningDialog := preload("res://src/ui_parts/import_warning_dialog.tscn")
+const AlertDialog := preload("res://src/ui_parts/alert_dialog.tscn")
+
+var has_overlay := false
+var overlay_ref: ColorRect
+
+
+func _ready() -> void:
+ get_window().files_dropped.connect(_on_files_dropped)
+ if OS.has_feature("web"):
+ _define_web_js()
+
+
+func _notification(what: int) -> void:
+ if what == NOTIFICATION_WM_WINDOW_FOCUS_IN:
+ _in_focus.emit()
+
+
+func _on_files_dropped(files: PackedStringArray) -> void:
+ if not has_overlay:
+ SVG.apply_svg_from_path(files[0])
+
+
+func add_overlay(overlay_menu: Node) -> void:
+ # A bit hacky, but I couldn't find out a better way at the time.
+ # I'm sure there is a better way of doing things though.
+ if has_overlay:
+ for child in overlay_ref.get_children():
+ child.tree_exiting.disconnect(remove_overlay)
+ child.queue_free()
+ if overlay_menu is Control:
+ overlay_menu.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
+ overlay_ref.add_child(overlay_menu)
+ overlay_menu.tree_exiting.connect(remove_overlay)
+ else:
+ overlay_ref = ColorRect.new()
+ overlay_ref.color = Color(0, 0, 0, 0.4)
+ get_tree().get_root().add_child(overlay_ref)
+ overlay_ref.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ if overlay_menu is Control:
+ overlay_menu.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
+ overlay_ref.add_child(overlay_menu)
+ overlay_menu.tree_exiting.connect(remove_overlay)
+ has_overlay = true
+ overlay_ref.process_mode = PROCESS_MODE_ALWAYS
+ get_tree().paused = true
+
+
+func remove_overlay() -> void:
+ overlay_ref.queue_free()
+ has_overlay = false
+ get_tree().paused = false
+
+
+func _input(event: InputEvent) -> void:
+ if event.is_action_pressed(&"import"):
+ get_viewport().set_input_as_handled()
+ SVG.open_import_dialog()
+ elif event.is_action_pressed(&"export"):
+ get_viewport().set_input_as_handled()
+ SVG.open_export_dialog()
+ elif event.is_action_pressed(&"save"):
+ get_viewport().set_input_as_handled()
+ SVG.open_save_dialog("svg", SVG.native_file_save, SVG.save_svg_to_file)
+
+
+func _unhandled_input(event) -> void:
+ if event.is_action_pressed(&"redo"):
+ get_viewport().set_input_as_handled()
+ SVG.redo()
+ elif event.is_action_pressed(&"undo"):
+ get_viewport().set_input_as_handled()
+ SVG.undo()
+
+ if get_viewport().gui_is_dragging():
+ return
+
+ if event.is_action_pressed(&"ui_cancel"):
+ Indications.clear_all_selections()
+ elif event.is_action_pressed(&"delete"):
+ Indications.delete_selected()
+ elif event.is_action_pressed(&"move_up"):
+ Indications.move_up_selected()
+ elif event.is_action_pressed(&"move_down"):
+ Indications.move_down_selected()
+ elif event.is_action_pressed(&"duplicate"):
+ Indications.duplicate_selected()
+ elif event.is_action_pressed(&"select_all"):
+ Indications.select_all()
+ elif event is InputEventKey:
+ Indications.respond_to_key_input(event)
+
+
+# Web file access code credit (Modified):
+# https://github.com/Pukkah/HTML5-File-Exchange-for-Godot
+# https://github.com/Orama-Interactive/Pixelorama/blob/master/src/Autoload/HTML5FileExchange.gd
+
+func web_load_svg() -> void:
+ JavaScriptBridge.eval("upload_svg();", true) # Open file dialog.
+ await _in_focus # Wait until dialog closed.
+ await get_tree().create_timer(1.5).timeout # Give some time for async JS data load.
+ if JavaScriptBridge.eval("canceled;", true):
+ return
+ var file_data
+ while true:
+ file_data = JavaScriptBridge.eval("fileData;", true)
+ if file_data != null:
+ break
+ await get_tree().create_timer(0.5).timeout
+
+ var file_name: String = JavaScriptBridge.eval("fileName;", true)
+ var extension := file_name.get_extension()
+ if extension == "svg":
+ var warning_panel := ImportWarningDialog.instantiate()
+ warning_panel.imported.connect(_import.bind(file_data, file_name))
+ warning_panel.set_svg(file_data)
+ HandlerGUI.add_overlay(warning_panel)
+ else:
+ var error := ""
+ if extension.is_empty():
+ error = "#file_open_empty_extension"
+ else:
+ error = tr(
+ &"#file_open_unsupported_extension").format({"passed_extension": extension})
+ var alert_dialog := AlertDialog.instantiate()
+ HandlerGUI.add_overlay(alert_dialog)
+ alert_dialog.setup(error, "#alert", 280.0)
+
+
+func _import(svg_text: String, file_name: String):
+ SVG.apply_svg_text(svg_text)
+ GlobalSettings.modify_save_data(&"current_file_path", file_name)
+ JavaScriptBridge.eval("fileData = undefined;", true)
+
+
+func web_save_svg() -> void:
+ JavaScriptBridge.download_buffer(
+ SVG.text.to_utf8_buffer(),
+ GlobalSettings.save_data.current_file_path.get_file()
+ )
+
+
+func web_save_png(img: Image) -> void:
+ JavaScriptBridge.download_buffer(
+ img.save_png_to_buffer(),
+ Utils.get_file_name(GlobalSettings.save_data.current_file_path) + ".png"
+ )
+
+
+func _define_web_js() -> void:
+ JavaScriptBridge.eval("""
+var fileData;
+var fileName;
+var canceled;
+var input = document.createElement('INPUT');
+input.setAttribute("type", "file");
+input.setAttribute("accept", ".svg");
+input.addEventListener('change', event => {
+ if (event.target.files.length == 0) {
+ return;
+ }
+ canceled = false;
+ var file = event.target.files[0];
+ var reader = new FileReader();
+ fileName = file.name;
+ reader.readAsText(file);
+ reader.onloadend = function(evt) {
+ if (evt.target.readyState == FileReader.DONE) {
+ fileData = evt.target.result;
+ }
+ }
+});
+
+function upload_svg() {
+ canceled = true;
+ input.click();
+};
+ """, true
+ )
diff --git a/src/Indications.gd b/src/Indications.gd
new file mode 100644
index 000000000..fc66f7f1d
--- /dev/null
+++ b/src/Indications.gd
@@ -0,0 +1,532 @@
+## This singleton handles editor information like zoom level and selections.
+extends Node
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const PathCommandPopup = preload("res://src/ui_elements/path_popup.tscn")
+
+const path_actions_dict := {
+ &"move_absolute": "M", &"move_relative": "m",
+ &"line_absolute": "L", &"line_relative": "l",
+ &"horizontal_line_absolute": "H", &"horizontal_line_relative": "h",
+ &"vertical_line_absolute": "V", &"vertical_line_relative": "v",
+ &"close_path_absolute": "Z", &"close_path_relative": "z",
+ &"elliptical_arc_absolute": "A", &"elliptical_arc_relative": "a",
+ &"cubic_bezier_absolute": "C", &"cubic_bezier_relative": "c",
+ &"shorthand_cubic_bezier_absolute": "S", &"shorthand_cubic_bezier_relative": "s",
+ &"quadratic_bezier_absolute": "Q", &"quadratic_bezier_relative": "q",
+ &"shorthand_quadratic_bezier_absolute": "T", &"shorthand_quadratic_bezier_relative": "t"
+}
+
+signal hover_changed
+signal selection_changed
+signal proposed_drop_changed
+
+# The viewport listens for this signal to put you in handle-placing mode.
+signal added_handle
+
+# The PackedInt32Array holds the hierarchical orders. TID means Tag ID.
+# For example, the 5th child of the 2nd child of the root tag would be (1, 4).
+# PackedInt32Array() means it's invalid.
+var hovered_tid := PackedInt32Array()
+var selected_tids: Array[PackedInt32Array] = []
+var selection_pivot_tid := PackedInt32Array()
+
+# Semi-hovered means the tag has inner selections, but it is not selected itself.
+# For example, individual path commands.
+# Note that you can't have a selected tag and an inner selection simultaneously!
+var semi_hovered_tid := PackedInt32Array()
+var semi_selected_tid := PackedInt32Array()
+# Inner stuff aren't in a tree, so they use an int. -1 means invalid.
+var inner_hovered := -1
+var inner_selections: Array[int] = []
+var inner_selection_pivot := -1
+
+# When dragging tags in the inspector.
+var proposed_drop_tid := PackedInt32Array()
+
+
+signal zoom_changed
+signal viewport_size_changed
+
+var zoom := 0.0
+var viewport_size := Vector2i.ZERO
+
+func set_zoom(new_value) -> void:
+ if zoom != new_value:
+ zoom = new_value
+ zoom_changed.emit()
+
+func set_viewport_size(new_value) -> void:
+ if viewport_size != new_value:
+ viewport_size = new_value
+ viewport_size_changed.emit()
+
+
+func _ready() -> void:
+ SVG.root_tag.tags_added.connect(_on_tags_added)
+ SVG.root_tag.tags_deleted.connect(_on_tags_deleted)
+ SVG.root_tag.tags_moved_in_parent.connect(_on_tags_moved_in_parent)
+ SVG.root_tag.tags_moved_to.connect(_on_tags_moved_to)
+ SVG.root_tag.changed_unknown.connect(clear_all_selections)
+
+
+## Override the selected tags with a single new selected tag.
+## If inner_idx is given, this will be an inner selection.
+func normal_select(tid: PackedInt32Array, inner_idx := -1) -> void:
+ if tid.is_empty():
+ return
+
+ if inner_idx == -1:
+ var old_selected_tids := selected_tids.duplicate()
+ if not semi_selected_tid.is_empty():
+ semi_selected_tid.clear()
+ inner_selections.clear()
+ if selected_tids.size() == 1 and selected_tids[0] == tid:
+ return
+ selection_pivot_tid = tid.duplicate()
+ selected_tids = [tid.duplicate()]
+ if old_selected_tids != selected_tids:
+ selection_changed.emit()
+ else:
+ selected_tids.clear()
+ var old_inner_selections := inner_selections.duplicate()
+ if semi_selected_tid == tid and\
+ inner_selections.size() == 1 and inner_selections[0] == inner_idx:
+ return
+ semi_selected_tid = tid.duplicate()
+ inner_selection_pivot = inner_idx
+ inner_selections = [inner_idx]
+ if inner_selections != old_inner_selections:
+ selection_changed.emit()
+
+## If the tag was selected, unselect it. If it was unselected, select it.
+## If inner_idx is given, this will be an inner selection.
+func ctrl_select(tid: PackedInt32Array, inner_idx := -1) -> void:
+ if tid.is_empty():
+ return
+
+ if inner_idx == -1:
+ inner_selections.clear()
+ var tid_idx := selected_tids.find(tid)
+ if tid_idx == -1:
+ selection_pivot_tid = tid.duplicate()
+ selected_tids.append(tid.duplicate())
+ else:
+ selected_tids.remove_at(tid_idx)
+ if selected_tids.is_empty():
+ selection_pivot_tid = PackedInt32Array()
+ else:
+ if semi_selected_tid != tid:
+ normal_select(tid, inner_idx)
+ else:
+ selected_tids.clear()
+ var idx_idx := inner_selections.find(inner_idx)
+ if idx_idx == -1:
+ inner_selection_pivot = inner_idx
+ inner_selections.append(inner_idx)
+ else:
+ inner_selections.remove_at(idx_idx)
+ if inner_selections.is_empty():
+ inner_selection_pivot = -1
+
+ selection_changed.emit()
+
+## Select all tags with the same depth from the tag to the last selected tag.
+## Similarly for inner selections if inner_idx is given, but without tree logic.
+func shift_select(tid: PackedInt32Array, inner_idx := -1) -> void:
+ if tid.is_empty():
+ return
+
+ if inner_idx == -1:
+ if selection_pivot_tid.is_empty():
+ if selected_tids.is_empty():
+ normal_select(tid, inner_idx)
+ return
+
+ if tid == selection_pivot_tid:
+ return
+
+ var old_selected_tids := selected_tids.duplicate()
+
+ if tid.size() != selection_pivot_tid.size():
+ if not tid in selected_tids:
+ selected_tids.append(tid)
+ selection_changed.emit()
+ return
+
+ var parent_tag := tid.duplicate()
+ parent_tag.resize(parent_tag.size() - 1)
+ var tid_idx := tid[-1]
+ var selection_pivot_tid_idx := selection_pivot_tid[-1]
+
+ var first_idx := mini(tid_idx, selection_pivot_tid_idx)
+ var last_idx := maxi(tid_idx, selection_pivot_tid_idx)
+ for i in range(first_idx, last_idx + 1):
+ var new_tid := parent_tag.duplicate()
+ new_tid.append(i)
+ if not new_tid in selected_tids:
+ selected_tids.append(new_tid)
+
+ if selected_tids == old_selected_tids:
+ return
+
+ else:
+ if inner_selection_pivot == -1:
+ if inner_selections.is_empty():
+ normal_select(tid, inner_idx)
+ return
+
+ var old_inner_selections := inner_selections.duplicate()
+ var first_idx := mini(inner_selection_pivot, inner_idx)
+ var last_idx := maxi(inner_selection_pivot, inner_idx)
+ for i in range(first_idx, last_idx + 1):
+ if not i in inner_selections:
+ inner_selections.append(i)
+
+ if inner_selections == old_inner_selections:
+ return
+
+ selection_changed.emit()
+
+## Select all tags.
+func select_all() -> void:
+ clear_inner_selection()
+ var tid_list := SVG.root_tag.get_all_tids()
+ if selected_tids == tid_list:
+ return
+
+ for tid in SVG.root_tag.get_all_tids():
+ if not tid in selected_tids:
+ selected_tids.append(tid)
+ selection_changed.emit()
+
+
+## Clear the selected tags.
+func clear_selection() -> void:
+ if not selected_tids.is_empty():
+ selected_tids.clear()
+ selection_pivot_tid.clear()
+ selection_changed.emit()
+
+## Clear the inner selection.
+func clear_inner_selection() -> void:
+ if not inner_selections.is_empty() or not semi_selected_tid.is_empty():
+ inner_selections.clear()
+ semi_selected_tid.clear()
+ inner_selection_pivot = -1
+ selection_changed.emit()
+
+## Clear the selected tags or the inner selection.
+func clear_all_selections() -> void:
+ if not inner_selections.is_empty() or not semi_selected_tid.is_empty() or\
+ not selected_tids.is_empty():
+ selected_tids.clear()
+ inner_selections.clear()
+ semi_selected_tid.clear()
+ selection_changed.emit()
+
+
+## Set the hovered tag.
+func set_hovered(tid: PackedInt32Array, inner_idx := -1) -> void:
+ if inner_idx == -1:
+ if hovered_tid != tid:
+ hovered_tid = tid.duplicate()
+ if not tid.is_empty():
+ inner_hovered = -1
+ semi_hovered_tid = PackedInt32Array()
+ hover_changed.emit()
+ else:
+ if semi_hovered_tid != tid:
+ semi_hovered_tid = tid.duplicate()
+ inner_hovered = inner_idx
+ if not tid.is_empty():
+ hovered_tid.clear()
+ hover_changed.emit()
+ elif inner_hovered != inner_idx:
+ inner_hovered = inner_idx
+ if not tid.is_empty():
+ hovered_tid.clear()
+ hover_changed.emit()
+
+## If the tag is hovered, make it not hovered.
+func remove_hovered(tid: PackedInt32Array, inner_idx := -1) -> void:
+ if inner_idx == -1:
+ if hovered_tid == tid:
+ hovered_tid.clear()
+ hover_changed.emit()
+ else:
+ if semi_hovered_tid == tid and inner_hovered == inner_idx:
+ semi_hovered_tid.clear()
+ inner_hovered = -1
+ hover_changed.emit()
+
+## Clear the hovered tag.
+func clear_hovered() -> void:
+ if not hovered_tid.is_empty():
+ hovered_tid.clear()
+ hover_changed.emit()
+
+## Clear the inner hover.
+func clear_inner_hovered() -> void:
+ if inner_hovered != -1:
+ inner_hovered = -1
+ hover_changed.emit()
+
+
+func set_proposed_drop_tid(tid: PackedInt32Array) -> void:
+ if proposed_drop_tid != tid:
+ proposed_drop_tid = tid.duplicate()
+ proposed_drop_changed.emit()
+
+func clear_proposed_drop_tid() -> void:
+ if not proposed_drop_tid.is_empty():
+ proposed_drop_tid.clear()
+ proposed_drop_changed.emit()
+
+
+func _on_tags_added(tids: Array[PackedInt32Array]) -> void:
+ selected_tids = tids.duplicate()
+
+# If selected tags were deleted, remove them from the list of selected tags.
+func _on_tags_deleted(tids: Array[PackedInt32Array]) -> void:
+ tids = tids.duplicate() # For some reason, it breaks without this.
+ var old_selected_tids := selected_tids.duplicate()
+ for deleted_tid in tids:
+ for i in range(selected_tids.size() - 1, -1, -1):
+ var tid := selected_tids[i]
+ if Utils.is_tid_parent_or_self(deleted_tid, tid):
+ selected_tids.remove_at(i)
+ if old_selected_tids != selected_tids:
+ selection_changed.emit()
+
+# If selected tags were moved up or down, change the TIDs and their children.
+func _on_tags_moved_in_parent(parent_tid: PackedInt32Array, indices: Array[int]) -> void:
+ var old_selected_tids := selected_tids.duplicate()
+ var tids_to_select: Array[PackedInt32Array] = []
+ var tids_to_unselect: Array[PackedInt32Array] = []
+
+ for index_idx in indices.size():
+ if index_idx == indices[index_idx]:
+ continue
+
+ # For the tags that have moved, get their old.
+ var old_moved_tid := parent_tid.duplicate()
+ old_moved_tid.append(indices[index_idx])
+
+ # If the TID or a child of it is found, append it.
+ for tid in selected_tids:
+ if Utils.is_tid_parent_or_self(old_moved_tid, tid):
+ var new_selected_tid := tid.duplicate()
+ new_selected_tid[parent_tid.size()] = index_idx
+ tids_to_unselect.append(tid)
+ tids_to_select.append(new_selected_tid)
+ for tid in tids_to_unselect:
+ selected_tids.erase(tid)
+ selected_tids += tids_to_select
+
+ if old_selected_tids != selected_tids:
+ selection_changed.emit()
+
+# If selected tags were moved to a location, change the TIDs and their children.
+func _on_tags_moved_to(tids: Array[PackedInt32Array], location: PackedInt32Array) -> void:
+ tids = tids.duplicate()
+ var new_selected_tids: Array[PackedInt32Array] = []
+ for moved_idx in tids.size():
+ var moved_tid := tids[moved_idx]
+ for tid in selected_tids:
+ if Utils.is_tid_parent_or_self(moved_tid, tid):
+ var new_location := Utils.get_parent_tid(location)
+ new_location.append(moved_idx + location[-1])
+ for ii in range(moved_tid.size(), tid.size()):
+ new_location.append(tid[ii])
+ new_selected_tids.append(new_location)
+ if selected_tids != new_selected_tids:
+ selected_tids = new_selected_tids
+ selection_changed.emit()
+
+
+func respond_to_key_input(event: InputEventKey) -> void:
+ # Path commands using keys.
+ if inner_selections.is_empty() or event.is_command_or_control_pressed():
+ # If a single path tag is selected, add the new command at the end.
+ if selected_tids.size() == 1:
+ var tag_ref := SVG.root_tag.get_tag(selected_tids[0])
+ if tag_ref.name == "path":
+ var path_attrib: AttributePath = tag_ref.attributes.d
+ for action_name in path_actions_dict.keys():
+ if event.is_action_pressed(action_name):
+ var path_cmd_count := path_attrib.get_command_count()
+ var path_cmd_char: String = path_actions_dict[action_name]
+ # Z after a Z is syntactically invalid.
+ if (path_cmd_count == 0 and not path_cmd_char in "Mm") or\
+ (path_cmd_char in "Zz" and path_cmd_count > 0 and\
+ path_attrib.get_command(path_cmd_count - 1) is\
+ PathCommand.CloseCommand):
+ return
+ path_attrib.insert_command(path_cmd_count, path_cmd_char)
+ normal_select(selected_tids[0], path_cmd_count)
+ added_handle.emit()
+ break
+ return
+ # If path commands are selected, insert after the last one.
+ for action_name in path_actions_dict.keys():
+ var tag_ref := SVG.root_tag.get_tag(semi_selected_tid)
+ if tag_ref.name == "path":
+ if event.is_action_pressed(action_name):
+ var path_attrib: AttributePath = tag_ref.attributes.d
+ var path_cmd_char: String = path_actions_dict[action_name]
+ var last_selection: int = inner_selections.max()
+ # Z after a Z is syntactically invalid.
+ if path_attrib.get_command(last_selection) is PathCommand.CloseCommand and\
+ path_cmd_char in "Zz":
+ return
+ path_attrib.insert_command(last_selection + 1, path_cmd_char)
+ normal_select(semi_selected_tid, last_selection + 1)
+ added_handle.emit()
+ break
+
+
+# Operations on selected tags.
+
+func delete_selected() -> void:
+ if not selected_tids.is_empty():
+ SVG.root_tag.delete_tags(selected_tids)
+ elif not inner_selections.is_empty() and not semi_selected_tid.is_empty():
+ inner_selections.sort()
+ inner_selections.reverse()
+ var tag_ref := SVG.root_tag.get_tag(semi_selected_tid)
+ match tag_ref.name:
+ "path": tag_ref.attributes.d.delete_commands(inner_selections)
+ clear_inner_selection()
+ clear_inner_hovered()
+
+func move_up_selected() -> void:
+ SVG.root_tag.move_tags_in_parent(selected_tids, false)
+
+func move_down_selected() -> void:
+ SVG.root_tag.move_tags_in_parent(selected_tids, true)
+
+func duplicate_selected() -> void:
+ SVG.root_tag.duplicate_tags(selected_tids)
+
+func insert_inner_after_selection(new_command: String) -> void:
+ var tag_ref := SVG.root_tag.get_tag(semi_selected_tid)
+ match tag_ref.name:
+ "path":
+ var path_attrib: AttributePath = tag_ref.attributes.d
+ var last_selection: int = inner_selections.max()
+ # Z after a Z is syntactically invalid.
+ if path_attrib.get_command(last_selection) is PathCommand.CloseCommand and\
+ new_command in "Zz":
+ return
+ path_attrib.insert_command(last_selection + 1, new_command)
+ normal_select(semi_selected_tid, last_selection + 1)
+
+
+func get_selection_context(popup_method: Callable) -> Popup:
+ var btn_arr: Array[Button] = []
+
+ if not selected_tids.is_empty():
+ var filtered_tids := Utils.filter_descendant_tids(selected_tids)
+ var can_move_down := true
+ var can_move_up := true
+ for base_tid in filtered_tids:
+ if not Utils.are_tid_parents_same(base_tid, filtered_tids[0]):
+ can_move_down = false
+ can_move_up = false
+ break
+
+ if can_move_up or can_move_down:
+ can_move_down = false
+ can_move_up = false
+ var parent_tid := Utils.get_parent_tid(filtered_tids[0])
+ var filtered_count := filtered_tids.size()
+ var parent_child_count := SVG.root_tag.get_tag(parent_tid).get_child_count()
+ for base_tid in filtered_tids:
+ if not can_move_up and base_tid[-1] >= filtered_count:
+ can_move_up = true
+ if not can_move_down and base_tid[-1] < parent_child_count - filtered_count:
+ can_move_down = true
+
+ btn_arr.append(Utils.create_btn(tr(&"#duplicate"), duplicate_selected,
+ false, load("res://visual/icons/Duplicate.svg")))
+
+ if selected_tids.size() == 1 and not SVG.root_tag.get_tag(
+ selected_tids[0]).possible_conversions.is_empty():
+ btn_arr.append(Utils.create_btn(tr(&"#convert_to"),
+ popup_convert_to_context.bind(popup_method), false,
+ load("res://visual/icons/Reload.svg")))
+
+ if can_move_up:
+ btn_arr.append(Utils.create_btn(tr(&"#move_up"), move_up_selected,
+ false, load("res://visual/icons/MoveUp.svg")))
+ if can_move_down:
+ btn_arr.append(Utils.create_btn(tr(&"#move_down"), move_down_selected,
+ false, load("res://visual/icons/MoveDown.svg")))
+
+ btn_arr.append(Utils.create_btn(tr(&"#delete"), delete_selected,
+ false, load("res://visual/icons/Delete.svg")))
+ elif not inner_selections.is_empty() and not semi_selected_tid.is_empty():
+ if inner_selections.size() == 1:
+ btn_arr.append(Utils.create_btn(tr(&"#insert_after"),
+ popup_insert_command_after_context.bind(popup_method),
+ false, load("res://visual/icons/Plus.svg")))
+ btn_arr.append(Utils.create_btn(tr(&"#convert_to"),
+ popup_convert_to_context.bind(popup_method), false,
+ load("res://visual/icons/Reload.svg")))
+
+ btn_arr.append(Utils.create_btn(tr(&"#delete"), delete_selected, false,
+ load("res://visual/icons/Delete.svg")))
+
+ var tag_context := ContextPopup.instantiate()
+ add_child(tag_context)
+ tag_context.set_button_array(btn_arr, true)
+ return tag_context
+
+func popup_convert_to_context(popup_method: Callable) -> void:
+ # The "Convert To" context popup.
+ if not selected_tids.is_empty():
+ var btn_arr: Array[Button] = []
+ var tag := SVG.root_tag.get_tag(selected_tids[0])
+ for tag_name in tag.possible_conversions:
+ var btn := Utils.create_btn(tag_name, convert_selected_tag_to.bind(tag_name),
+ !tag.can_replace(tag_name), load("res://visual/icons/tag/%s.svg" % tag_name))
+ btn.add_theme_font_override(&"font", load("res://visual/fonts/FontMono.ttf"))
+ btn_arr.append(btn)
+ var context_popup := ContextPopup.instantiate()
+ add_child(context_popup)
+ context_popup.set_button_array(btn_arr, true)
+ popup_method.call(context_popup)
+ elif not inner_selections.is_empty() and not semi_selected_tid.is_empty():
+ var cmd_char: String = SVG.root_tag.get_tag(semi_selected_tid).\
+ attributes.d.get_command(inner_selections[0]).command_char
+ var command_picker := PathCommandPopup.instantiate()
+ add_child(command_picker)
+ command_picker.force_relativity(Utils.is_string_lower(cmd_char))
+ command_picker.disable_invalid([cmd_char.to_upper()])
+ command_picker.path_command_picked.connect(convert_selected_command_to)
+ popup_method.call(command_picker)
+
+func popup_insert_command_after_context(popup_method: Callable) -> void:
+ var cmd_char: String = SVG.root_tag.get_tag(semi_selected_tid).attributes.d.\
+ get_command(inner_selections.max()).command_char
+
+ var command_picker := PathCommandPopup.instantiate()
+ add_child(command_picker)
+ match cmd_char.to_upper():
+ "M": command_picker.disable_invalid(["M", "Z", "T"])
+ "Z": command_picker.disable_invalid(["Z"])
+ "L", "H", "V", "A": command_picker.disable_invalid(["S", "T"])
+ "C", "S": command_picker.disable_invalid(["T"])
+ "Q", "T": command_picker.disable_invalid(["S"])
+ command_picker.path_command_picked.connect(insert_inner_after_selection)
+ popup_method.call(command_picker)
+
+func convert_selected_tag_to(tag_name: String) -> void:
+ var tid := selected_tids[0]
+ SVG.root_tag.replace_tag(tid, SVG.root_tag.get_tag(tid).get_replacement(tag_name))
+
+func convert_selected_command_to(cmd_type: String) -> void:
+ var tag_ref := SVG.root_tag.get_tag(semi_selected_tid)
+ match tag_ref.name:
+ "path": tag_ref.attributes.d.convert_command(inner_selections[0], cmd_type)
diff --git a/src/SVG.gd b/src/SVG.gd
index ed10839a2..224e0d4c1 100644
--- a/src/SVG.gd
+++ b/src/SVG.gd
@@ -1,61 +1,166 @@
+## This singleton handles the two representations of the SVG:
+## The SVG text, and the native [TagSVG] representation.
extends Node
-const display_path := "user://display.svg"
-
-var string := ""
-var data := SVGData.new()
-@onready var texture_node: TextureRect = get_tree().current_scene.get_node(^"%Texture")
-@onready var code_editor: CodeEdit = get_tree().current_scene.get_node(^"%CodeEdit")
-
-func tags_to_string(superscaled := false, add_xmlns := false) -> String:
- var w := data.w
- var h := data.h
- var scale := 1024.0/maxi(w, h) if (superscaled and w != 0 and h != 0) else 1.0
- # Opening
- string = ''
- if superscaled:
- string += '' % scale
- # Inner tags
- for tag in data.tags:
- string += '<' + tag.title
- for attribute_key in tag.attributes:
- var attribute: SVGAttribute = tag.attributes[attribute_key]
- if attribute.value == attribute.default:
- continue
-
- match attribute.type:
- SVGAttribute.Type.INT:
- string += ' %s="%d"' % [attribute_key, attribute.value]
- SVGAttribute.Type.FLOAT, SVGAttribute.Type.UFLOAT, SVGAttribute.Type.NFLOAT:
- string += ' %s="' % attribute_key + String.num(attribute.value, 4) + '"'
- SVGAttribute.Type.COLOR:
- if attribute.value == "none":
- string += ' %s="%s"' % [attribute_key, attribute.value]
- else:
- string += ' %s="#%s"' % [attribute_key, attribute.value]
- string += '/>'
- # Closing
- if superscaled:
- string += ' '
- string += ' '
- return string
-
-func string_to_tags() -> void:
- pass # TODO
-
-
-func update() -> void:
- # Store the SVG string.
- var file := FileAccess.open(display_path, FileAccess.WRITE)
- file.store_string(tags_to_string(true))
- file.close()
- code_editor.text = tags_to_string()
- # Update the display.
- if FileAccess.file_exists(display_path):
- var image := Image.load_from_file(display_path)
- var image_texture := ImageTexture.create_from_image(image)
- texture_node.texture = image_texture
+const AlertDialog := preload("res://src/ui_parts/alert_dialog.tscn")
+const ImportWarningDialog = preload("res://src/ui_parts/import_warning_dialog.tscn")
+const SVGFileDialog = preload("res://src/ui_parts/svg_file_dialog.tscn")
+const ExportDialog = preload("res://src/ui_parts/export_dialog.tscn")
+
+var text := ""
+var root_tag := TagSVG.new()
+
+var UR := UndoRedo.new()
+
+signal parsing_finished(error_id: SVGParser.ParseError)
+
+func _ready() -> void:
+ UR.version_changed.connect(_on_undo_redo)
+ root_tag.changed_unknown.connect(update_text.bind(false))
+ root_tag.attribute_changed.connect(update_text)
+ root_tag.child_attribute_changed.connect(update_text)
+ root_tag.tag_layout_changed.connect(update_text)
+
+ var cmdline_args := OS.get_cmdline_args()
+ var load_cmdl := false
+ if not (OS.is_debug_build() and not OS.has_feature("template")) and\
+ cmdline_args.size() >= 1:
+ load_cmdl = true
+
+ await get_tree().get_root().ready # Await tree ready to be able to add error dialogs.
+
+ # Guarantee a proper SVG text first, as the import warnings dialog
+ # that might pop up from command line file opening is cancellable.
+ if not GlobalSettings.save_data.svg_text.is_empty():
+ apply_svg_text(GlobalSettings.save_data.svg_text)
+ else:
+ root_tag.attributes.width.set_num(16.0)
+ root_tag.attributes.height.set_num(16.0)
+ update_text(false)
+
+ if load_cmdl:
+ apply_svg_from_path(cmdline_args[0])
+
+ UR.clear_history()
+
+
+func update_tags() -> void:
+ var svg_parse_result := SVGParser.text_to_svg(text)
+ parsing_finished.emit(svg_parse_result.error)
+ if svg_parse_result.error == SVGParser.ParseError.OK:
+ root_tag.replace_self(SVGParser.text_to_svg(text).svg)
+
+
+func update_text(undo_redo := true) -> void:
+ if undo_redo:
+ UR.create_action("")
+ UR.add_do_property(self, &"text", SVGParser.svg_to_text(root_tag))
+ UR.add_undo_property(self, &"text", GlobalSettings.save_data.svg_text)
+ UR.commit_action()
+ GlobalSettings.modify_save_data(&"svg_text", text)
+ else:
+ text = SVGParser.svg_to_text(root_tag)
+
+func undo() -> void:
+ if UR.has_undo():
+ UR.undo()
+ update_tags()
+
+func redo() -> void:
+ if UR.has_redo():
+ UR.redo()
+ update_tags()
+
+func _on_undo_redo() -> void:
+ GlobalSettings.modify_save_data(&"svg_text", text)
+
+
+func open_import_dialog() -> void:
+ # Open it inside a native file dialog, or our custom one if it's not available.
+ if DisplayServer.has_feature(DisplayServer.FEATURE_NATIVE_DIALOG):
+ DisplayServer.file_dialog_show("Import a .svg file", Utils.get_last_dir(), "", false,
+ DisplayServer.FILE_DIALOG_MODE_OPEN_FILE, ["*.svg"], native_file_import)
+ elif OS.has_feature("web"):
+ HandlerGUI.web_load_svg()
+ else:
+ var svg_import_dialog := SVGFileDialog.instantiate()
+ svg_import_dialog.current_dir = Utils.get_last_dir()
+ HandlerGUI.add_overlay(svg_import_dialog)
+ svg_import_dialog.file_selected.connect(apply_svg_from_path)
+
+func native_file_import(has_selected: bool, files: PackedStringArray,
+_filter_idx: int) -> void:
+ if has_selected:
+ apply_svg_from_path(files[0])
+
+
+func open_export_dialog() -> void:
+ HandlerGUI.add_overlay(ExportDialog.instantiate())
+
+func open_save_dialog(extension: String, native_callable: Callable,
+non_native_callable: Callable) -> void:
+ # Open it inside a native file dialog, or our custom one if it's not available.
+ if DisplayServer.has_feature(DisplayServer.FEATURE_NATIVE_DIALOG):
+ DisplayServer.file_dialog_show("Save the .%s file" % extension,
+ Utils.get_last_dir(),
+ Utils.get_file_name(GlobalSettings.save_data.current_file_path) + "." + extension,
+ false, DisplayServer.FILE_DIALOG_MODE_SAVE_FILE,
+ ["*." + extension], native_callable)
+ elif OS.has_feature("web"):
+ HandlerGUI.web_save_svg()
+ else:
+ var svg_export_dialog := SVGFileDialog.instantiate()
+ svg_export_dialog.current_dir = Utils.get_last_dir()
+ svg_export_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
+ HandlerGUI.add_overlay(svg_export_dialog)
+ svg_export_dialog.file_selected.connect(non_native_callable)
+
+func native_file_save(has_selected: bool, files: PackedStringArray,
+_filter_idx: int) -> void:
+ if has_selected:
+ save_svg_to_file(files[0])
+
+
+func apply_svg_from_path(path: String) -> int:
+ var svg_file := FileAccess.open(path, FileAccess.READ)
+ var error := ""
+ var extension := path.get_extension()
+
+ GlobalSettings.modify_save_data(&"last_used_dir", path.get_base_dir())
+
+ if extension.is_empty():
+ error = "#file_open_empty_extension"
+ elif extension == "tscn":
+ return ERR_FILE_CANT_OPEN
+ elif extension != "svg":
+ error = tr(
+ &"#file_open_unsupported_extension").format({"passed_extension": extension})
+ elif svg_file == null:
+ error = "#file_open_fail_message"
+
+ if not error.is_empty():
+ var alert_dialog := AlertDialog.instantiate()
+ HandlerGUI.add_overlay(alert_dialog)
+ alert_dialog.setup(error, "#alert", 280.0)
+ return ERR_FILE_CANT_OPEN
+
+ var svg_text := svg_file.get_as_text()
+ var warning_panel := ImportWarningDialog.instantiate()
+ warning_panel.imported.connect(finish_import.bind(svg_text, path))
+ warning_panel.set_svg(svg_text)
+ HandlerGUI.add_overlay(warning_panel)
+ return OK
+
+func finish_import(svg_text: String, file_path: String) -> void:
+ GlobalSettings.modify_save_data(&"current_file_path", file_path)
+ apply_svg_text(svg_text)
+
+
+func save_svg_to_file(path: String) -> void:
+ var FA := FileAccess.open(path, FileAccess.WRITE)
+ FA.store_string(text)
+
+func apply_svg_text(svg_text: String,) -> void:
+ text = svg_text
+ GlobalSettings.modify_save_data(&"svg_text", text)
+ update_tags()
diff --git a/src/Utils.gd b/src/Utils.gd
new file mode 100644
index 000000000..90cd71da1
--- /dev/null
+++ b/src/Utils.gd
@@ -0,0 +1,211 @@
+class_name Utils extends RefCounted
+
+const path_command_char_dict = {
+ "M": "Move to", "L": "Line to", "H": "Horizontal Line to", "V": "Vertical Line to",
+ "Z": "Close Path", "A": "Elliptical Arc to", "Q": "Quadratic Bezier to",
+ "T": "Shorthand Quadratic Bezier to", "C": "Cubic Bezier to",
+ "S": "Shorthand Cubic Bezier to"
+}
+
+# Enum with values to be used for set_value() of attribute editors.
+# REGULAR means that the attribute will update if the new value is different.
+# INTERMEDIATE and FINAL cause the attribute update to have the corresponding sync mode.
+# FINAL also causes the equivalence check to be skipped.
+enum UpdateType {REGULAR, INTERMEDIATE, FINAL}
+
+enum InteractionType {NONE = 0, HOVERED = 1, SELECTED = 2, HOVERED_SELECTED = 3}
+
+
+static func is_string_upper(string: String) -> bool:
+ return string.to_upper() == string
+
+static func is_string_lower(string: String) -> bool:
+ return string.to_lower() == string
+
+static func get_file_name(string: String) -> String:
+ return string.get_file().trim_suffix("." + string.get_extension())
+
+
+# Resize the control to be resized automatically to its text width, up to a maximum.
+# The property name defaults account for most controls that may need to use this.
+static func set_max_text_width(control: Control, max_width: float, buffer: float,
+text_property := &"text", font_property := &"font",
+font_size_property := &"font_size") -> void:
+ control.custom_minimum_size.x = minf(control.get_theme_font(
+ font_property).get_string_size(control.get(text_property),
+ HORIZONTAL_ALIGNMENT_FILL, -1,
+ control.get_theme_font_size(font_size_property)).x + buffer, max_width)
+
+# Should usually be the global rect of a control.
+static func popup_under_rect(popup: Popup, rect: Rect2, viewport: Viewport) -> void:
+ var screen_transform := viewport.get_screen_transform()
+ var screen_h := viewport.get_visible_rect().size.y
+ var popup_pos := Vector2.ZERO
+ # Popup below if there's enough space or we're in the bottom half of the screen.
+ if rect.position.y + rect.size.y + popup.size.y < screen_h or\
+ rect.position.y + rect.size.y / 2 <= screen_h / 2.0:
+ popup_pos.y = rect.position.y + rect.size.y
+ else:
+ popup_pos.y = rect.position.y - popup.size.y
+ # Horizontal alignment and other things.
+ popup_pos.x = rect.position.x
+ popup_pos += screen_transform.get_origin() / screen_transform.get_scale()
+ popup.popup(Rect2(popup_pos, popup.size))
+
+# Should usually be the global rect of a control.
+static func popup_under_rect_center(popup: Popup, rect: Rect2, viewport: Viewport) -> void:
+ var screen_transform := viewport.get_screen_transform()
+ var screen_h := viewport.get_visible_rect().size.y
+ var popup_pos := Vector2.ZERO
+ # Popup below if there's enough space or we're in the bottom half of the screen.
+ if rect.position.y + rect.size.y + popup.size.y < screen_h or\
+ rect.position.y + rect.size.y / 2 <= screen_h / 2.0:
+ popup_pos.y = rect.position.y + rect.size.y
+ else:
+ popup_pos.y = rect.position.y - popup.size.y
+ # Align horizontally and other things.
+ popup_pos.x = rect.position.x - popup.size.x / 2.0 + rect.size.x / 2
+ popup_pos += screen_transform.get_origin() / screen_transform.get_scale()
+ popup.popup(Rect2(popup_pos, popup.size))
+
+# Should usually be the global position of the mouse.
+static func popup_under_pos(popup: Popup, pos: Vector2, viewport: Viewport) -> void:
+ var screen_transform := viewport.get_screen_transform()
+ pos += screen_transform.get_origin() / screen_transform.get_scale()
+ popup.popup(Rect2(pos, popup.size))
+
+static func create_btn(text: String, press_action: Callable, disabled := false,
+icon: Texture2D = null) -> Button:
+ var btn := Button.new()
+ btn.text = text
+ if icon != null:
+ btn.icon = icon
+ if disabled:
+ btn.disabled = true
+ else:
+ btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ btn.pressed.connect(press_action)
+ return btn
+
+static func create_checkbox(text: String, toggle_action: Callable,
+start_pressed: bool) -> CheckBox:
+ var checkbox := CheckBox.new()
+ checkbox.text = text
+ checkbox.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ checkbox.button_pressed = start_pressed
+ checkbox.pressed.connect(toggle_action)
+ return checkbox
+
+
+static func get_cubic_bezier_points(cp1: Vector2, cp2: Vector2, cp3: Vector2,
+cp4: Vector2) -> PackedVector2Array:
+ var curve := Curve2D.new()
+ curve.add_point(cp1, Vector2(), cp2)
+ curve.add_point(cp4, cp3)
+ return curve.tessellate(6, 1)
+
+static func get_quadratic_bezier_points(cp1: Vector2, cp2: Vector2,
+cp3: Vector2) -> PackedVector2Array:
+ return Utils.get_cubic_bezier_points(
+ cp1, 2/3.0 * (cp2 - cp1), 2/3.0 * (cp2 - cp3), cp3)
+
+# Ellipse parametric equation.
+static func E(c: Vector2, r: Vector2, cosine: float, sine: float, t: float) -> Vector2:
+ var xt := r.x * cos(t)
+ var yt := r.y * sin(t)
+ return c + Vector2(xt * cosine - yt * sine, xt * sine + yt * cosine)
+
+# Ellipse parametric equation derivative (for tangents).
+static func Et(r: Vector2, cosine: float, sine: float, t: float) -> Vector2:
+ var xt := -r.x * sin(t)
+ var yt := r.y * cos(t)
+ return Vector2(xt * cosine - yt * sine, xt * sine + yt * cosine)
+
+
+# [1] > [1, 2] > [1, 0] > [0]
+static func compare_tids(tid1: PackedInt32Array, tid2: PackedInt32Array) -> bool:
+ var smaller_tid_size := mini(tid1.size(), tid2.size())
+ for i in smaller_tid_size:
+ if tid1[i] < tid2[i]:
+ return true
+ elif tid1[i] > tid2[i]:
+ return false
+ return tid1.size() > smaller_tid_size
+
+static func compare_tids_r(tid1: PackedInt32Array, tid2: PackedInt32Array) -> bool:
+ return compare_tids(tid2, tid1)
+
+# Indirect parent, i.e. ancestor. Passing the root tag as parent will return false.
+static func is_tid_parent(parent: PackedInt32Array, child: PackedInt32Array) -> bool:
+ if parent.is_empty():
+ return false
+ var parent_size := parent.size()
+ if parent_size >= child.size():
+ return false
+
+ for i in parent_size:
+ if parent[i] != child[i]:
+ return false
+ return true
+
+static func is_tid_parent_or_self(parent: PackedInt32Array,
+child: PackedInt32Array) -> bool:
+ return is_tid_parent(parent, child) or parent == child
+
+static func get_parent_tid(tid: PackedInt32Array) -> PackedInt32Array:
+ var parent_tid := tid.duplicate()
+ parent_tid.resize(tid.size() - 1)
+ return parent_tid
+
+static func are_tid_parents_same(tid1: PackedInt32Array, tid2: PackedInt32Array) -> bool:
+ if tid1.size() != tid2.size():
+ return false
+ for i in tid1.size() - 1:
+ if tid1[i] != tid2[i]:
+ return false
+ return true
+
+# Filter out all descendants.
+static func filter_descendant_tids(tids: Array[PackedInt32Array]) -> Array[PackedInt32Array]:
+ var new_tids: Array[PackedInt32Array] = tids.duplicate()
+ new_tids.sort_custom(Utils.compare_tids_r)
+ # Linear scan to filter out the descendants.
+ var last_accepted := new_tids[0]
+ var i := 1
+ while i < new_tids.size():
+ var tid := new_tids[i]
+ if Utils.is_tid_parent_or_self(last_accepted, tid):
+ new_tids.remove_at(i)
+ else:
+ last_accepted = new_tids[i]
+ i += 1
+ return new_tids
+
+
+static func is_event_drag(event: InputEvent) -> bool:
+ return event is InputEventMouseMotion and event.button_mask == MOUSE_BUTTON_LEFT
+
+static func is_event_drag_start(event: InputEvent) -> bool:
+ return event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and\
+ event.is_pressed()
+
+static func is_event_drag_end(event: InputEvent) -> bool:
+ return event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and\
+ event.is_released()
+
+static func is_event_cancel(event: InputEvent) -> bool:
+ return event.is_action_pressed(&"ui_cancel") or\
+ event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT
+
+# Used to somewhat prevent unwanted inputs from triggering tag drag & drop.
+static func mouse_filter_pass_non_drag_events(event: InputEvent) -> Control.MouseFilter:
+ return Control.MOUSE_FILTER_STOP if event is InputEventMouseMotion and\
+ event.button_mask == MOUSE_BUTTON_MASK_LEFT else Control.MOUSE_FILTER_PASS
+
+
+static func get_last_dir() -> String:
+ if GlobalSettings.save_data.last_used_dir.is_empty()\
+ or not DirAccess.dir_exists_absolute(GlobalSettings.save_data.last_used_dir):
+ return OS.get_system_dir(OS.SYSTEM_DIR_PICTURES)
+ else:
+ return GlobalSettings.save_data.last_used_dir
diff --git a/src/data_classes/Attribute.gd b/src/data_classes/Attribute.gd
new file mode 100644
index 000000000..9c7dc95aa
--- /dev/null
+++ b/src/data_classes/Attribute.gd
@@ -0,0 +1,48 @@
+## Abstract class for an attribute inside a [Tag], i.e.
+class_name Attribute extends RefCounted
+
+signal value_changed(new_value: String)
+signal propagate_value_changed(undo_redo: bool)
+
+var default: String
+var _value: String
+
+enum SyncMode {LOUD, INTERMEDIATE, FINAL, NO_PROPAGATION, SILENT}
+
+# LOUD means the attribute will emit value_changed and be noticed everywhere.
+
+# INTERMEDIATE is the same as LOUD, but doesn't create an UndoRedo action.
+# Can be used to update an attribute continuously (i.e. dragging a color).
+
+# FINAL is the same as LOUD, but it runs even if the new value is the same.
+# This can be used to force an UndoRedo action after some intermediate changes.
+# Note that the attribute is not responsible for making sure the new value is
+# different from the previous one in the UndoRedo, this must be handled in the widgets.
+
+# NO_PROPAGATION means the tag won't learn about it. This can allow the attribute change
+# to be noted by an attribute editor without the SVG text being updated.
+# This can be used, for example, to update two attributes corresponding to 2D coordinates
+# without the first one causing an update to the SVG text.
+
+# SILENT means the attribute update is ignored fully. It only makes sense
+# if there is logic for updating the corresponding attribute editor despite that.
+
+func set_value(new_value: String, sync_mode := SyncMode.LOUD) -> void:
+ var proposed_new_value := autoformat(new_value)
+ if proposed_new_value != _value or sync_mode == SyncMode.FINAL:
+ _value = proposed_new_value
+ _sync()
+ if sync_mode != SyncMode.SILENT:
+ value_changed.emit(proposed_new_value)
+ if sync_mode != SyncMode.NO_PROPAGATION:
+ propagate_value_changed.emit(sync_mode != SyncMode.INTERMEDIATE)
+
+func get_value() -> String:
+ return _value
+
+# Override these functions in extending classes to update the non-string representation.
+func _sync() -> void:
+ return
+
+func autoformat(text: String) -> String:
+ return text
diff --git a/src/data_classes/AttributeColor.gd b/src/data_classes/AttributeColor.gd
new file mode 100644
index 000000000..be11a01c4
--- /dev/null
+++ b/src/data_classes/AttributeColor.gd
@@ -0,0 +1,171 @@
+## An attribute representing a color string, or an url to an ID.
+class_name AttributeColor extends Attribute
+
+# No direct color representation for this attribute type. There are too many quirks.
+
+func _init(new_default: String, new_init := "") -> void:
+ default = new_default
+ set_value(new_init if !new_init.is_empty() else new_default, SyncMode.SILENT)
+
+func set_value(new_value: String, sync_mode := SyncMode.LOUD) -> void:
+ super(new_value if ColorParser.is_valid(new_value) else default, sync_mode)
+
+func autoformat(text: String) -> String:
+ if GlobalSettings.color_enable_autoformatting:
+ var new_text := ColorParser.format_text(text)
+ return default if ColorParser.are_colors_same(new_text, default) else new_text
+ else:
+ return text
+
+
+const special_colors = ["none", "currentColor"]
+
+const named_colors = { # Dictionary{String: String}
+ "aliceblue": "#f0f8ff",
+ "antiquewhite": "#faebd7",
+ "aqua": "#00ffff",
+ "aquamarine": "#7fffd4",
+ "azure": "#f0ffff",
+ "beige": "#f5f5dc",
+ "bisque": "#ffe4c4",
+ "black": "#000000",
+ "blanchedalmond": "#ffebcd",
+ "blue": "#0000ff",
+ "blueviolet": "#8a2be2",
+ "brown": "#a52a2a",
+ "burlywood": "#deb887",
+ "cadetblue": "#5f9ea0",
+ "chartreuse": "#7fff00",
+ "chocolate": "#d2691e",
+ "coral": "#ff7f50",
+ "cornflowerblue": "#6495ed",
+ "cornsilk": "#fff8dc",
+ "crimson": "#dc143c",
+ "cyan": "#00ffff",
+ "darkblue": "#00008b",
+ "darkcyan": "#008b8b",
+ "darkgoldenrod": "#b8860b",
+ "darkgray": "#a9a9a9",
+ "darkgreen": "#006400",
+ "darkgrey": "#a9a9a9",
+ "darkkhaki": "#bdb76b",
+ "darkmagenta": "#8b008b",
+ "darkolivegreen": "#556b2f",
+ "darkorange": "#ff8c00",
+ "darkorchid": "#9932cc",
+ "darkred": "#8b0000",
+ "darksalmon": "#e9967a",
+ "darkseagreen": "#8fbc8f",
+ "darkslateblue": "#483d8b",
+ "darkslategray": "#2f4f4f",
+ "darkslategrey": "#2f4f4f",
+ "darkturquoise": "#00ced1",
+ "darkviolet": "#9400d3",
+ "deeppink": "#ff1493",
+ "deepskyblue": "#00bfff",
+ "dimgray": "#696969",
+ "dimgrey": "#696969",
+ "dodgerblue": "#1e90ff",
+ "firebrick": "#b22222",
+ "floralwhite": "#fffaf0",
+ "forestgreen": "#228b22",
+ "fuchsia": "#ff00ff",
+ "gainsboro": "#dcdcdc",
+ "ghostwhite": "#f8f8ff",
+ "gold": "#ffd700",
+ "goldenrod": "#daa520",
+ "gray": "#808080",
+ "green": "#008000",
+ "greenyellow": "#adff2f",
+ "grey": "#808080",
+ "honeydew": "#f0fff0",
+ "hotpink": "#ff69b4",
+ "indianred": "#cd5c5c",
+ "indigo": "#4b0082",
+ "ivory": "#fffff0",
+ "khaki": "#f0e68c",
+ "lavender": "#e6e6fa",
+ "lavenderblush": "#fff0f5",
+ "lawngreen": "#7cfc00",
+ "lemonchiffon": "#fffacd",
+ "lightblue": "#add8e6",
+ "lightcoral": "#f08080",
+ "lightcyan": "#e0ffff",
+ "lightgoldenrodyellow": "#fafad2",
+ "lightgray": "#d3d3d3",
+ "lightgreen": "#90ee90",
+ "lightgrey": "#d3d3d3",
+ "lightpink": "#ffb6c1",
+ "lightsalmon": "#ffa07a",
+ "lightseagreen": "#20b2aa",
+ "lightskyblue": "#87cefa",
+ "lightslategray": "#778899",
+ "lightslategrey": "#778899",
+ "lightsteelblue": "#b0c4de",
+ "lightyellow": "#ffffe0",
+ "lime": "#00ff00",
+ "limegreen": "#32cd32",
+ "linen": "#faf0e6",
+ "magenta": "#ff00ff",
+ "maroon": "#800000",
+ "mediumaquamarine": "#66cdaa",
+ "mediumblue": "#0000cd",
+ "mediumorchid": "#ba55d3",
+ "mediumpurple": "#9370db",
+ "mediumseagreen": "#3cb371",
+ "mediumslateblue": "#7b68ee",
+ "mediumspringgreen": "#00fa9a",
+ "mediumturquoise": "#48d1cc",
+ "mediumvioletred": "#c71585",
+ "midnightblue": "#191970",
+ "mintcream": "#f5fffa",
+ "mistyrose": "#ffe4e1",
+ "moccasin": "#ffe4b5",
+ "navajowhite": "#ffdead",
+ "navy": "#000080",
+ "oldlace": "#fdf5e6",
+ "olive": "#808000",
+ "olivedrab": "#6b8e23",
+ "orange": "#ffa500",
+ "orangered": "#ff4500",
+ "orchid": "#da70d6",
+ "palegoldenrod": "#eee8aa",
+ "palegreen": "#98fb98",
+ "paleturquoise": "#afeeee",
+ "palevioletred": "#db7093",
+ "papayawhip": "#ffefd5",
+ "peachpuff": "#ffdab9",
+ "peru": "#cd853f",
+ "pink": "#ffc0cb",
+ "plum": "#dda0dd",
+ "powderblue": "#b0e0e6",
+ "purple": "#800080",
+ "red": "#ff0000",
+ "rosybrown": "#bc8f8f",
+ "royalblue": "#4169e1",
+ "saddlebrown": "#8b4513",
+ "salmon": "#fa8072",
+ "sandybrown": "#f4a460",
+ "seagreen": "#2e8b57",
+ "seashell": "#fff5ee",
+ "sienna": "#a0522d",
+ "silver": "#c0c0c0",
+ "skyblue": "#87ceeb",
+ "slateblue": "#6a5acd",
+ "slategray": "#708090",
+ "slategrey": "#708090",
+ "snow": "#fffafa",
+ "springgreen": "#00ff7f",
+ "steelblue": "#4682b4",
+ "tan": "#d2b48c",
+ "teal": "#008080",
+ "thistle": "#d8bfd8",
+ "tomato": "#ff6347",
+ "turquoise": "#40e0d0",
+ "violet": "#ee82ee",
+ "wheat": "#f5deb3",
+ "white": "#ffffff",
+ "whitesmoke": "#f5f5f5",
+ "yellow": "#ffff00",
+ "yellowgreen": "#9acd32",
+}
diff --git a/src/data_classes/AttributeEnum.gd b/src/data_classes/AttributeEnum.gd
new file mode 100644
index 000000000..38fd0716a
--- /dev/null
+++ b/src/data_classes/AttributeEnum.gd
@@ -0,0 +1,12 @@
+## An attribute with only a set of meaningful values.
+class_name AttributeEnum extends Attribute
+
+var possible_values: Array[String]
+
+func _init(new_possible_values: Array[String], new_default_idx := 0) -> void:
+ possible_values = new_possible_values
+ default = possible_values[new_default_idx]
+ set_value(default, SyncMode.SILENT)
+
+func set_value(new_value: String, sync_mode := SyncMode.LOUD) -> void:
+ super(new_value if new_value in possible_values else default, sync_mode)
diff --git a/src/data_classes/AttributeList.gd b/src/data_classes/AttributeList.gd
new file mode 100644
index 000000000..5df3d54f7
--- /dev/null
+++ b/src/data_classes/AttributeList.gd
@@ -0,0 +1,34 @@
+## An attribute representing a list of numbers.
+class_name AttributeList extends Attribute
+
+var _list: PackedFloat32Array
+
+func _init() -> void:
+ default = ""
+ set_value("", SyncMode.SILENT)
+
+func _sync() -> void:
+ _list = ListParser.string_to_list(get_value())
+
+func set_list(new_list: PackedFloat32Array, sync_mode := SyncMode.LOUD) -> void:
+ _list = new_list
+ super.set_value(ListParser.list_to_string(new_list), sync_mode)
+
+func get_list() -> PackedFloat32Array:
+ return _list
+
+func get_list_size() -> int:
+ return _list.size()
+
+# Just a helper to handle Rect2.
+func set_rect(new_rect: Rect2, sync_mode := SyncMode.LOUD) -> void:
+ set_list(PackedFloat32Array([new_rect.position.x, new_rect.position.y,
+ new_rect.size.x, new_rect.size.y]), sync_mode)
+
+
+func set_list_element(idx: int, new_value: float, sync_mode := SyncMode.LOUD) -> void:
+ _list[idx] = new_value
+ set_value(ListParser.list_to_string(_list), sync_mode)
+
+func get_list_element(idx: int) -> float:
+ return _list[idx] if idx < _list.size() else NAN
diff --git a/src/data_classes/AttributeNumeric.gd b/src/data_classes/AttributeNumeric.gd
new file mode 100644
index 000000000..07896f368
--- /dev/null
+++ b/src/data_classes/AttributeNumeric.gd
@@ -0,0 +1,51 @@
+## An attribute representing a number.
+class_name AttributeNumeric extends Attribute
+
+var _number := NAN
+enum Mode {FLOAT, UFLOAT, NFLOAT} # UFLOAT is positive-only, NFLOAT is in [0, 1].
+var mode := Mode.FLOAT
+
+func _init(new_mode: Mode, new_default: String, new_init := "") -> void:
+ mode = new_mode
+ default = new_default
+ set_value(new_init if !new_init.is_empty() else new_default, SyncMode.SILENT)
+
+func _sync() -> void:
+ _number = NumberParser.text_to_num(get_value())
+
+func autoformat(text: String) -> String:
+ if GlobalSettings.number_enable_autoformatting:
+ return NumberParser.format_text(text)
+ else:
+ return text
+
+func set_num(new_number: float, sync_mode := SyncMode.LOUD) -> void:
+ _number = new_number
+ super.set_value(NumberParser.num_to_text(new_number)\
+ if is_finite(_number) else "", sync_mode)
+
+func get_num() -> float:
+ return _number
+
+
+# This function evaluates expressions even if "," or ";" is used as a decimal separator.
+static func evaluate_expr(text: String) -> float:
+ text = text.trim_prefix("+") # Expression can't handle unary plus.
+
+ var expr := Expression.new()
+ var err := expr.parse(text.replace(",", "."))
+ if err == OK:
+ var result: float = expr.execute()
+ if not expr.has_execute_failed():
+ return result
+ err = expr.parse(text.replace(";", "."))
+ if err == OK:
+ var result: float = expr.execute()
+ if not expr.has_execute_failed():
+ return result
+ err = expr.parse(text)
+ if err == OK:
+ var result: float = expr.execute()
+ if not expr.has_execute_failed():
+ return result
+ return NAN
diff --git a/src/data_classes/AttributePath.gd b/src/data_classes/AttributePath.gd
new file mode 100644
index 000000000..6e152fd7c
--- /dev/null
+++ b/src/data_classes/AttributePath.gd
@@ -0,0 +1,218 @@
+## The "d" attribute of [TagPath].
+class_name AttributePath extends Attribute
+
+var _commands: Array[PathCommand]
+
+func _init() -> void:
+ default = ""
+ set_value(default, SyncMode.SILENT)
+
+func _sync() -> void:
+ _commands = PathDataParser.parse_path_data(get_value())
+ locate_start_points()
+
+func autoformat(text: String) -> String:
+ if GlobalSettings.path_enable_autoformatting:
+ return PathDataParser.path_commands_to_text(PathDataParser.parse_path_data(text))
+ else:
+ return text
+
+
+func set_commands(new_commands: Array[PathCommand], sync_mode := SyncMode.LOUD) -> void:
+ _commands = new_commands
+ sync_after_commands_change(sync_mode)
+
+func sync_after_commands_change(sync_mode := SyncMode.LOUD) -> void:
+ set_value(PathDataParser.path_commands_to_text(_commands), sync_mode)
+
+
+func locate_start_points() -> void:
+ # Start points are absolute.
+ var last_end_point := Vector2.ZERO
+ var current_subpath_start := Vector2.ZERO
+ for command in _commands:
+ command.start = last_end_point
+
+ if command is PathCommand.MoveCommand:
+ current_subpath_start = command.start if command.relative else Vector2.ZERO
+ current_subpath_start += Vector2(command.x, command.y)
+ elif command is PathCommand.CloseCommand:
+ last_end_point = current_subpath_start
+ continue
+
+ # Prepare for the next iteration.
+ if command.relative:
+ if &"x" in command:
+ last_end_point.x += command.x
+ if &"y" in command:
+ last_end_point.y += command.y
+ else:
+ if &"x" in command:
+ last_end_point.x = command.x
+ if &"y" in command:
+ last_end_point.y = command.y
+
+
+func get_command_count() -> int:
+ return _commands.size()
+
+func get_command(idx: int) -> PathCommand:
+ return _commands[idx]
+
+# Return the start and end indices of the subpath.
+func get_subpath(idx: int) -> Vector2i:
+ var output := Vector2i(idx, idx)
+ # Subpaths start from the last M command, or the commmand after the last Z command.
+ while output.x > 0:
+ if get_command(output.x) is PathCommand.MoveCommand or\
+ get_command(output.x - 1) is PathCommand.CloseCommand:
+ break
+ output.x -= 1
+ while output.y < get_command_count() - 1:
+ if get_command(output.y + 1) is PathCommand.MoveCommand or\
+ get_command(output.y) is PathCommand.CloseCommand:
+ break
+ output.y += 1
+ return output
+
+func get_implied_S_control(cmd_idx: int) -> Vector2:
+ var cmd := get_command(cmd_idx)
+ var prev_cmd := get_command(cmd_idx - 1)
+ var v := Vector2.ZERO if cmd.relative else cmd.start
+ if prev_cmd.command_char in "CcSs":
+ var prev_control_pt := Vector2(prev_cmd.x2, prev_cmd.y2)
+ v = (cmd.start if cmd.relative else cmd.start * 2) - prev_control_pt
+ if prev_cmd.relative:
+ v -= prev_cmd.start
+ return v
+
+func get_implied_T_control(idx: int) -> Vector2:
+ var prevQ_idx := idx - 1
+ var prevQ_cmd := get_command(prevQ_idx)
+ while prevQ_idx >= 0:
+ if not prevQ_cmd.command_char in "Tt":
+ break
+ else:
+ prevQ_idx -= 1
+ prevQ_cmd = get_command(prevQ_idx)
+ if prevQ_idx == -1:
+ return Vector2(NAN, NAN)
+
+ var prevQ_x: float = prevQ_cmd.x if &"x" in prevQ_cmd else prevQ_cmd.start.x
+ var prevQ_y: float = prevQ_cmd.y if &"y" in prevQ_cmd else prevQ_cmd.start.y
+ var prevQ_v := Vector2(prevQ_x, prevQ_y)
+ var prevQ_v1 := Vector2(prevQ_cmd.x1, prevQ_cmd.y1) if\
+ prevQ_cmd.command_char in "Qq" else prevQ_v
+ var prevQ_end := prevQ_cmd.start + prevQ_v if prevQ_cmd.relative else prevQ_v
+ var prevQ_control_pt := prevQ_cmd.start + prevQ_v1 if prevQ_cmd.relative else prevQ_v1
+
+ var v := prevQ_end * 2 - prevQ_control_pt
+ for T_idx in range(prevQ_idx + 1, idx):
+ var T_cmd := get_command(T_idx)
+ var T_v := Vector2(T_cmd.x, T_cmd.y)
+ var T_end := T_cmd.start + T_v if T_cmd.relative else T_v
+ v = T_end * 2 - v
+
+ var cmd := get_command(idx)
+ if cmd.relative:
+ v -= cmd.start
+ return v
+
+
+func set_command_property(idx: int, property: StringName, new_value: float,
+sync_mode := SyncMode.LOUD) -> void:
+ var cmd := get_command(idx)
+ if cmd.get(property) != new_value or sync_mode == SyncMode.FINAL:
+ cmd.set(property, new_value)
+ sync_after_commands_change(sync_mode)
+
+func insert_command(idx: int, command_char: String, vec := Vector2.ZERO,
+sync_mode := SyncMode.LOUD) -> void:
+ var new_cmd: PathCommand = PathCommand.translation_dict[command_char.to_upper()].new()
+ var relative := Utils.is_string_lower(command_char)
+ if relative:
+ new_cmd.toggle_relative()
+ _commands.insert(idx, new_cmd)
+ locate_start_points()
+ if not command_char in "Zz":
+ if not command_char in "Vv":
+ new_cmd.x = vec.x
+ if not command_char in "Hh":
+ new_cmd.y = vec.y
+ if command_char in "Qq":
+ new_cmd.x1 = lerpf(0.0 if relative else new_cmd.start.x, vec.x, 0.5)
+ new_cmd.y1 = lerpf(0.0 if relative else new_cmd.start.y, vec.y, 0.5)
+ elif command_char in "Ss":
+ new_cmd.x2 = lerpf(0.0 if relative else new_cmd.start.x, vec.x, 2/3.0)
+ new_cmd.y2 = lerpf(0.0 if relative else new_cmd.start.y, vec.y, 2/3.0)
+ elif command_char in "Cc":
+ new_cmd.x1 = lerpf(0.0 if relative else new_cmd.start.x, vec.x, 1/3.0)
+ new_cmd.y1 = lerpf(0.0 if relative else new_cmd.start.y, vec.y, 1/3.0)
+ new_cmd.x2 = lerpf(0.0 if relative else new_cmd.start.x, vec.x, 2/3.0)
+ new_cmd.y2 = lerpf(0.0 if relative else new_cmd.start.y, vec.y, 2/3.0)
+ sync_after_commands_change(sync_mode)
+
+
+func convert_command(idx: int, command_char: String, sync_mode := SyncMode.LOUD) -> void:
+ var old_cmd := get_command(idx)
+ if old_cmd.command_char == command_char:
+ return
+
+ var cmd_absolute_char := command_char.to_upper()
+ var new_cmd: PathCommand = PathCommand.translation_dict[cmd_absolute_char].new()
+ for property in [&"x", &"y", &"x1", &"y1", &"x2", &"y2"]:
+ if property in old_cmd and property in new_cmd:
+ new_cmd[property] = old_cmd[property]
+
+ var relative := Utils.is_string_lower(command_char)
+
+ if &"x" in new_cmd and not &"x" in old_cmd:
+ new_cmd.x = 0.0 if relative else old_cmd.start.x
+ if &"y" in new_cmd and not &"y" in old_cmd:
+ new_cmd.y = 0.0 if relative else old_cmd.start.y
+
+ match cmd_absolute_char:
+ "C":
+ if old_cmd.command_char in "Ss":
+ var v := get_implied_S_control(idx)
+ new_cmd.x1 = v.x
+ new_cmd.y1 = v.y
+ else:
+ new_cmd.x1 = lerpf(0.0 if relative else old_cmd.start.x, new_cmd.x, 1/3.0)
+ new_cmd.y1 = lerpf(0.0 if relative else old_cmd.start.y, new_cmd.y, 1/3.0)
+ new_cmd.x2 = lerpf(0.0 if relative else old_cmd.start.x, new_cmd.x, 2/3.0)
+ new_cmd.y2 = lerpf(0.0 if relative else old_cmd.start.y, new_cmd.y, 2/3.0)
+ "S":
+ if not old_cmd.command_char in "Cc":
+ new_cmd.x2 = lerpf(0.0 if relative else old_cmd.start.x, new_cmd.x, 2/3.0)
+ new_cmd.y2 = lerpf(0.0 if relative else old_cmd.start.y, new_cmd.y, 2/3.0)
+ "Q":
+ if old_cmd.command_char in "Tt":
+ var v := get_implied_T_control(idx)
+ new_cmd.x1 = v.x
+ new_cmd.y1 = v.y
+ else:
+ new_cmd.x1 = lerpf(0.0 if relative else old_cmd.start.x, new_cmd.x, 0.5)
+ new_cmd.y1 = lerpf(0.0 if relative else old_cmd.start.y, new_cmd.y, 0.5)
+
+ _commands.remove_at(idx)
+ _commands.insert(idx, new_cmd)
+ if relative:
+ _commands[idx].toggle_relative()
+ sync_after_commands_change(sync_mode)
+
+
+func delete_commands(indices: Array[int], sync_mode := SyncMode.LOUD) -> void:
+ if indices.is_empty():
+ return
+
+ indices = indices.duplicate()
+ indices.sort()
+ indices.reverse()
+ for idx in indices:
+ _commands.remove_at(idx)
+ sync_after_commands_change(sync_mode)
+
+func toggle_relative_command(idx: int, sync_mode := SyncMode.LOUD) -> void:
+ _commands[idx].toggle_relative()
+ sync_after_commands_change(sync_mode)
diff --git a/src/data_classes/AttributeTransform.gd b/src/data_classes/AttributeTransform.gd
new file mode 100644
index 000000000..38c48f498
--- /dev/null
+++ b/src/data_classes/AttributeTransform.gd
@@ -0,0 +1,148 @@
+## An attribute representing a list of transforms.
+class_name AttributeTransform extends Attribute
+
+class Transform extends RefCounted:
+ func compute_transform() -> Transform2D:
+ return Transform2D.IDENTITY
+
+class TransformMatrix extends Transform:
+ var x1: float
+ var x2: float
+ var y1: float
+ var y2: float
+ var o1: float
+ var o2: float
+
+ func _init(new_x1: float, new_x2: float, new_y1: float, new_y2: float, new_o1: float,
+ new_o2: float) -> void:
+ x1 = new_x1
+ x2 = new_x2
+ y1 = new_y1
+ y2 = new_y2
+ o1 = new_o1
+ o2 = new_o2
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2(x1, x2), Vector2(y1, y2), Vector2(o1, o2))
+
+class TransformTranslate extends Transform:
+ var x: float
+ var y: float
+
+ func _init(new_x: float, new_y: float) -> void:
+ x = new_x
+ y = new_y
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2.RIGHT, Vector2.DOWN, Vector2(x, y))
+
+class TransformRotate extends Transform:
+ var deg: float
+ var x: float
+ var y: float
+
+ func _init(new_deg: float, new_x: float, new_y: float) -> void:
+ deg = new_deg
+ x = new_x
+ y = new_y
+
+ func compute_transform() -> Transform2D:
+ var pt := Vector2(x, y)
+ return Transform2D.IDENTITY.translated(-pt).rotated(deg_to_rad(deg)).translated(pt)
+
+class TransformScale extends Transform:
+ var x: float
+ var y: float
+
+ func _init(new_x: float, new_y: float) -> void:
+ x = new_x
+ y = new_y
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2.RIGHT * x, Vector2.DOWN * y, Vector2.ZERO)
+
+class TransformSkewX extends Transform:
+ var x: float
+
+ func _init(new_x: float) -> void:
+ x = new_x
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2.RIGHT, Vector2(tan(deg_to_rad(x)), 1), Vector2.ZERO)
+
+class TransformSkewY extends Transform:
+ var y: float
+
+ func _init(new_y: float) -> void:
+ y = new_y
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2(1, tan(deg_to_rad(y))), Vector2.DOWN, Vector2.ZERO)
+
+
+var _transform_list: Array[Transform] = []
+var _final_transform := Transform2D.IDENTITY
+
+func _init() -> void:
+ default = ""
+ set_value(default, SyncMode.SILENT)
+
+func _sync() -> void:
+ _transform_list = TransformListParser.text_to_transform_list(get_value())
+ _final_transform = AttributeTransform.compute_final_transform(_transform_list)
+
+func sync_after_transforms_change(sync_mode := SyncMode.LOUD) -> void:
+ super.set_value(TransformListParser.transform_list_to_text(_transform_list), sync_mode)
+
+func autoformat(text: String) -> String:
+ if GlobalSettings.transform_enable_autoformatting:
+ return TransformListParser.transform_list_to_text(
+ TransformListParser.text_to_transform_list(text))
+ else:
+ return text
+
+func set_transform_list(new_transform_list: Array[Transform],
+sync_mode := SyncMode.LOUD) -> void:
+ _transform_list = new_transform_list
+ _final_transform = AttributeTransform.compute_final_transform(new_transform_list)
+ super.set_value(TransformListParser.transform_list_to_text(new_transform_list),
+ sync_mode)
+
+func set_transform_property(idx: int, property: StringName, new_value: float,
+sync_mode := SyncMode.LOUD) -> void:
+ if _transform_list[idx].get(property) != new_value or sync_mode == SyncMode.FINAL:
+ _transform_list[idx].set(property, new_value)
+ sync_after_transforms_change(sync_mode)
+
+func get_transform_list() -> Array[Transform]:
+ return _transform_list
+
+func get_transform_count() -> int:
+ return _transform_list.size()
+
+func get_transform(idx: int) -> Transform:
+ return _transform_list[idx]
+
+func get_final_transform() -> Transform2D:
+ return _final_transform
+
+
+static func compute_final_transform(transform_list: Array[Transform]) -> Transform2D:
+ var final_transform := Transform2D.IDENTITY
+ for t in transform_list:
+ final_transform *= t.compute_transform()
+ return final_transform
+
+func delete_transform(idx: int) -> void:
+ _transform_list.remove_at(idx)
+ sync_after_transforms_change()
+
+func insert_transform(idx: int, type: String) -> void:
+ match type:
+ "matrix": _transform_list.insert(idx, TransformMatrix.new(1, 0, 0, 1, 0, 0))
+ "translate": _transform_list.insert(idx, TransformTranslate.new(0, 0))
+ "rotate": _transform_list.insert(idx, TransformRotate.new(0, 0, 0))
+ "scale": _transform_list.insert(idx, TransformScale.new(1, 1))
+ "skewX": _transform_list.insert(idx, TransformSkewX.new(0))
+ "skewY": _transform_list.insert(idx, TransformSkewY.new(0))
+ sync_after_transforms_change()
diff --git a/src/data_classes/AttributeUnknown.gd b/src/data_classes/AttributeUnknown.gd
new file mode 100644
index 000000000..d99b02820
--- /dev/null
+++ b/src/data_classes/AttributeUnknown.gd
@@ -0,0 +1,9 @@
+## An attribute not recognized by GodSVG.
+class_name AttributeUnknown extends Attribute
+
+var name := ""
+
+func _init(new_name: String, new_init := "") -> void:
+ default = ""
+ name = new_name
+ set_value(new_init, SyncMode.SILENT)
diff --git a/src/data_classes/ColorPalette.gd b/src/data_classes/ColorPalette.gd
new file mode 100644
index 000000000..e280f5495
--- /dev/null
+++ b/src/data_classes/ColorPalette.gd
@@ -0,0 +1,8 @@
+class_name ColorPalette extends Resource
+
+@export var name: String # Color palettes must be uniquely named.
+@export var named_colors: Array[NamedColor] # Array because consistent order is helpful.
+
+func _init(new_name := "", new_colors: Array[NamedColor] = []) -> void:
+ name = new_name
+ named_colors = new_colors
diff --git a/src/data_classes/NamedColor.gd b/src/data_classes/NamedColor.gd
new file mode 100644
index 000000000..01dc5de30
--- /dev/null
+++ b/src/data_classes/NamedColor.gd
@@ -0,0 +1,8 @@
+class_name NamedColor extends Resource
+
+@export var name: String # Names don't need to be unique.
+@export var color: String
+
+func _init(new_color := "", new_name := "") -> void:
+ color = new_color
+ name = new_name
diff --git a/src/data_classes/PathCommand.gd b/src/data_classes/PathCommand.gd
new file mode 100644
index 000000000..45f6fa534
--- /dev/null
+++ b/src/data_classes/PathCommand.gd
@@ -0,0 +1,155 @@
+## A native class that represents a path command and its parameters.
+class_name PathCommand extends RefCounted
+
+const translation_dict := {
+ "M": MoveCommand, "L": LineCommand, "H": HorizontalLineCommand,
+ "V": VerticalLineCommand, "Z": CloseCommand, "A": EllipticalArcCommand,
+ "Q": QuadraticBezierCommand, "T": ShorthandQuadraticBezierCommand,
+ "C": CubicBezierCommand, "S": ShorthandCubicBezierCommand
+}
+
+var command_char := ""
+var arg_count := 0
+var relative := false
+var start: Vector2
+func toggle_relative() -> void:
+ if relative:
+ relative = false
+ command_char = command_char.to_upper()
+ for property in [&"x", &"x1", &"x2"]:
+ if property in self:
+ set(property, start.x + get(property))
+ for property in [&"y", &"y1", &"y2"]:
+ if property in self:
+ set(property, start.y + get(property))
+ else:
+ relative = true
+ command_char = command_char.to_lower()
+ for property in [&"x", &"x1", &"x2"]:
+ if property in self:
+ set(property, get(property) - start.x)
+ for property in [&"y", &"y1", &"y2"]:
+ if property in self:
+ set(property, get(property) - start.y)
+
+
+class MoveCommand extends PathCommand:
+ var x: float
+ var y: float
+ func _init(new_x := 0.0, new_y := 0.0, p_rel := false) -> void:
+ relative = p_rel
+ command_char = "m" if p_rel else "M"
+ arg_count = 2
+ x = new_x
+ y = new_y
+
+class LineCommand extends PathCommand:
+ var x: float
+ var y: float
+ func _init(new_x := 0.0, new_y := 0.0, p_rel := false) -> void:
+ relative = p_rel
+ command_char = "l" if p_rel else "L"
+ arg_count = 2
+ x = new_x
+ y = new_y
+
+class HorizontalLineCommand extends PathCommand:
+ var x: float
+ func _init(new_x := 0.0, p_rel := false) -> void:
+ relative = p_rel
+ command_char = "h" if p_rel else "H"
+ arg_count = 1
+ x = new_x
+
+class VerticalLineCommand extends PathCommand:
+ var y: float
+ func _init(new_y := 0.0, p_rel := false) -> void:
+ relative = p_rel
+ command_char = "v" if p_rel else "V"
+ arg_count = 1
+ y = new_y
+
+class EllipticalArcCommand extends PathCommand:
+ var rx: float
+ var ry: float
+ var rot: float
+ var large_arc_flag: int
+ var sweep_flag: int
+ var x: float
+ var y: float
+ func _init(new_rx := 1.0, new_ry := 1.0, new_rot := 0.0, new_large_arc_flag := 0,
+ new_sweep_flag := 0, new_x := 0.0, new_y := 0.0, p_rel := false) -> void:
+ relative = p_rel
+ command_char = "a" if p_rel else "A"
+ arg_count = 7
+ rx = new_rx
+ ry = new_ry
+ rot = new_rot
+ large_arc_flag = new_large_arc_flag
+ sweep_flag = new_sweep_flag
+ x = new_x
+ y = new_y
+
+class QuadraticBezierCommand extends PathCommand:
+ var x1: float
+ var y1: float
+ var x: float
+ var y: float
+ func _init(new_x1 := 0.0, new_y1 := 0.0, new_x := 0.0, new_y := 0.0,
+ p_rel := false) -> void:
+ relative = p_rel
+ command_char = "q" if p_rel else "Q"
+ arg_count = 4
+ x1 = new_x1
+ y1 = new_y1
+ x = new_x
+ y = new_y
+
+class ShorthandQuadraticBezierCommand extends PathCommand:
+ var x: float
+ var y: float
+ func _init(new_x := 0.0, new_y := 0.0, p_rel := false) -> void:
+ relative = p_rel
+ command_char = "t" if p_rel else "T"
+ arg_count = 2
+ x = new_x
+ y = new_y
+
+class CubicBezierCommand extends PathCommand:
+ var x1: float
+ var y1: float
+ var x2: float
+ var y2: float
+ var x: float
+ var y: float
+ func _init(new_x1 := 0.0, new_y1 := 0.0, new_x2 := 0.0, new_y2 := 0.0,
+ new_x := 0.0, new_y := 0.0, p_rel := false) -> void:
+ relative = p_rel
+ command_char = "c" if p_rel else "C"
+ arg_count = 6
+ x1 = new_x1
+ y1 = new_y1
+ x2 = new_x2
+ y2 = new_y2
+ x = new_x
+ y = new_y
+
+class ShorthandCubicBezierCommand extends PathCommand:
+ var x2: float
+ var y2: float
+ var x: float
+ var y: float
+ func _init(new_x2 := 0.0, new_y2 := 0.0, new_x := 0.0, new_y := 0.0,
+ p_rel := false) -> void:
+ relative = p_rel
+ command_char = "s" if p_rel else "S"
+ arg_count = 4
+ x2 = new_x2
+ y2 = new_y2
+ x = new_x
+ y = new_y
+
+class CloseCommand extends PathCommand:
+ func _init(p_rel := false) -> void:
+ relative = p_rel
+ command_char = "z" if p_rel else "Z"
diff --git a/src/data_classes/SVGAttribute.gd b/src/data_classes/SVGAttribute.gd
deleted file mode 100644
index 79c6b43b9..000000000
--- a/src/data_classes/SVGAttribute.gd
+++ /dev/null
@@ -1,12 +0,0 @@
-class_name SVGAttribute extends RefCounted
-
-enum Type {INT, FLOAT, UFLOAT, NFLOAT, COLOR, PATH_DEFINITION}
-
-var type: Type
-var value: Variant
-var default: Variant
-
-func _init(new_type: Type, new_default: Variant):
- type = new_type
- default = new_default
- value = new_default
diff --git a/src/data_classes/SVGData.gd b/src/data_classes/SVGData.gd
deleted file mode 100644
index 6e39d4aa0..000000000
--- a/src/data_classes/SVGData.gd
+++ /dev/null
@@ -1,5 +0,0 @@
-class_name SVGData extends RefCounted
-
-var w: int
-var h: int
-var tags: Array[SVGTag]
diff --git a/src/data_classes/SVGTag.gd b/src/data_classes/SVGTag.gd
deleted file mode 100644
index a30072288..000000000
--- a/src/data_classes/SVGTag.gd
+++ /dev/null
@@ -1,4 +0,0 @@
-class_name SVGTag extends RefCounted
-
-var title: String
-var attributes: Dictionary # Dictionary{String: SVGAttribute}
diff --git a/src/data_classes/SVGTagCircle.gd b/src/data_classes/SVGTagCircle.gd
deleted file mode 100644
index 0bdde2d63..000000000
--- a/src/data_classes/SVGTagCircle.gd
+++ /dev/null
@@ -1,12 +0,0 @@
-class_name SVGTagCircle extends SVGTag
-
-func _init():
- title = "circle"
- attributes = {
- "cx": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "cy": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "r": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "fill": SVGAttribute.new(SVGAttribute.Type.COLOR, "000"),
- "stroke": SVGAttribute.new(SVGAttribute.Type.COLOR, "none"),
- "stroke-width": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 1.0),
- }
diff --git a/src/data_classes/SVGTagEllipse.gd b/src/data_classes/SVGTagEllipse.gd
deleted file mode 100644
index 44cdb793a..000000000
--- a/src/data_classes/SVGTagEllipse.gd
+++ /dev/null
@@ -1,13 +0,0 @@
-class_name SVGTagEllipse extends SVGTag
-
-func _init():
- title = "ellipse"
- attributes = {
- "cx": SVGAttribute.new(SVGAttribute.Type.FLOAT, 0.0),
- "cy": SVGAttribute.new(SVGAttribute.Type.FLOAT, 0.0),
- "rx": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "ry": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "fill": SVGAttribute.new(SVGAttribute.Type.COLOR, "000"),
- "stroke": SVGAttribute.new(SVGAttribute.Type.COLOR, "none"),
- "stroke-width": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 1.0),
- }
diff --git a/src/data_classes/SVGTagPath.gd b/src/data_classes/SVGTagPath.gd
deleted file mode 100644
index fb15ac95e..000000000
--- a/src/data_classes/SVGTagPath.gd
+++ /dev/null
@@ -1,10 +0,0 @@
-class_name SVGTagPath extends SVGTag
-
-func _init():
- title = "path"
- attributes = {
- "d": SVGAttribute.new(SVGAttribute.Type.PATH_DEFINITION, 0.0),
- "fill": SVGAttribute.new(SVGAttribute.Type.COLOR, "000"),
- "stroke": SVGAttribute.new(SVGAttribute.Type.COLOR, "none"),
- "stroke-width": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 1.0),
- }
diff --git a/src/data_classes/SVGTagRect.gd b/src/data_classes/SVGTagRect.gd
deleted file mode 100644
index 799d530b0..000000000
--- a/src/data_classes/SVGTagRect.gd
+++ /dev/null
@@ -1,15 +0,0 @@
-class_name SVGTagRect extends SVGTag
-
-func _init():
- title = "rect"
- attributes = {
- "x": SVGAttribute.new(SVGAttribute.Type.FLOAT, 0.0),
- "y": SVGAttribute.new(SVGAttribute.Type.FLOAT, 0.0),
- "width": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "height": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "rx": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "ry": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 0.0),
- "fill": SVGAttribute.new(SVGAttribute.Type.COLOR, "000"),
- "stroke": SVGAttribute.new(SVGAttribute.Type.COLOR, "none"),
- "stroke-width": SVGAttribute.new(SVGAttribute.Type.UFLOAT, 1.0),
- }
diff --git a/src/data_classes/SaveData.gd b/src/data_classes/SaveData.gd
new file mode 100644
index 000000000..ae45b03c1
--- /dev/null
+++ b/src/data_classes/SaveData.gd
@@ -0,0 +1,19 @@
+## Stores data that needs to be retained between sessions.
+class_name SaveData extends Resource
+
+const GoodColorPicker = preload("res://src/ui_elements/good_color_picker.gd")
+
+@export var window_mode := DisplayServer.WINDOW_MODE_MAXIMIZED
+@export var svg_text := ""
+@export var viewbox_coupling := true
+@export var snap := -0.5 # Negative when disabled.
+@export var color_picker_slider_mode := GoodColorPicker.SliderMode.RGB
+@export var path_command_relative := false
+@export var last_used_dir := ""
+
+signal current_file_path_changed
+@export var current_file_path := "":
+ set(new_value):
+ if new_value != current_file_path:
+ current_file_path = new_value
+ current_file_path_changed.emit()
diff --git a/src/data_classes/SavedColorPalettes.gd b/src/data_classes/SavedColorPalettes.gd
new file mode 100644
index 000000000..9a0901796
--- /dev/null
+++ b/src/data_classes/SavedColorPalettes.gd
@@ -0,0 +1,3 @@
+class_name SavedColorPalettes extends Resource
+
+@export var palettes: Array[ColorPalette]
diff --git a/src/data_classes/Tag.gd b/src/data_classes/Tag.gd
new file mode 100644
index 000000000..5e254e3c4
--- /dev/null
+++ b/src/data_classes/Tag.gd
@@ -0,0 +1,61 @@
+## A SVG tag, standalone ([code] [/code]) or container ([code] [/code]).
+class_name Tag extends RefCounted
+
+var child_tags: Array[Tag]
+
+signal attribute_changed(undo_redo: bool)
+
+var attributes: Dictionary # Dictionary{String: Attribute}
+
+# Attributes that aren't recognized (usually because GodSVG doesn't support them).
+var unknown_attributes: Array[AttributeUnknown]
+
+func is_standalone() -> bool:
+ return child_tags.is_empty()
+
+func _init():
+ for attribute: Attribute in attributes.values():
+ attribute.propagate_value_changed.connect(emit_attribute_changed)
+
+func set_unknown_attributes(attribs: Array[AttributeUnknown]) -> void:
+ unknown_attributes = attribs.duplicate()
+ for attribute in unknown_attributes:
+ attribute.propagate_value_changed.connect(emit_attribute_changed)
+
+func emit_attribute_changed(undo_redo: bool) -> void:
+ attribute_changed.emit(undo_redo)
+
+func get_child_count() -> int:
+ return child_tags.size()
+
+
+# Why is there no way to duplicate RefCounteds, again?
+func create_duplicate() -> Tag:
+ var type: GDScript = get_script()
+ var new_tag: Tag
+ if type == TagUnknown:
+ new_tag = TagUnknown.new(self.name)
+ else:
+ new_tag = type.new()
+ for attribute in new_tag.attributes:
+ new_tag.attributes[attribute].set_value(attributes[attribute].get_value())
+ var unknown_attributes_array: Array[AttributeUnknown] = []
+ for attribute in unknown_attributes:
+ var new_attrib := AttributeUnknown.new(attribute.name)
+ new_attrib.set_value(attribute.get_value())
+ unknown_attributes_array.append(new_attrib)
+ new_tag.set_unknown_attributes(unknown_attributes_array)
+
+ # Iterate this over all children.
+ var new_child_tags: Array[Tag] = []
+ for tag in child_tags:
+ new_child_tags.append(tag.create_duplicate())
+ new_tag.child_tags = new_child_tags
+ return new_tag
+
+# To be overridden in extending classes.
+func can_replace(_new_tag: String) -> bool:
+ return false
+
+func get_replacement(_new_tag: String) -> Tag:
+ return null
diff --git a/src/data_classes/TagCircle.gd b/src/data_classes/TagCircle.gd
new file mode 100644
index 000000000..b809ab39c
--- /dev/null
+++ b/src/data_classes/TagCircle.gd
@@ -0,0 +1,76 @@
+## A tag.
+class_name TagCircle extends Tag
+
+const name = "circle"
+const possible_conversions = ["ellipse", "rect", "path"]
+const icon = preload("res://visual/icons/tag/circle.svg")
+
+const known_shape_attributes = ["cx", "cy", "r"]
+const known_inheritable_attributes = ["transform", "opacity", "fill", "fill-opacity",
+ "stroke", "stroke-opacity", "stroke-width"]
+
+func _init(pos := Vector2.ZERO) -> void:
+ attributes = {
+ "transform": AttributeTransform.new(),
+ "cx": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "cy": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "r": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "0", "1"),
+ "opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "fill": AttributeColor.new("black"),
+ "fill-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke": AttributeColor.new("none"),
+ "stroke-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke-width": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "1"),
+ }
+ attributes.cx.set_num(pos.x)
+ attributes.cy.set_num(pos.y)
+ super()
+
+
+func can_replace(new_tag: String) -> bool:
+ return new_tag in ["ellipse", "rect", "path"]
+
+func get_replacement(new_tag: String) -> Tag:
+ if not can_replace(new_tag):
+ return null
+
+ var tag: Tag
+ var retained_attributes: Array[String] = []
+ match new_tag:
+ "ellipse":
+ tag = TagEllipse.new()
+ retained_attributes = ["cx", "cy", "transform", "opacity", "fill",
+ "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]
+ tag.attributes.rx.set_num(attributes.r.get_num(), Attribute.SyncMode.SILENT)
+ tag.attributes.ry.set_num(attributes.r.get_num(), Attribute.SyncMode.SILENT)
+ "rect":
+ tag = TagRect.new()
+ retained_attributes = ["transform", "opacity", "fill", "fill-opacity", "stroke",
+ "stroke-opacity", "stroke-width"]
+ tag.attributes.x.set_num(attributes.cx.get_num() - attributes.r.get_num(),
+ Attribute.SyncMode.SILENT)
+ tag.attributes.y.set_num(attributes.cy.get_num() - attributes.r.get_num(),
+ Attribute.SyncMode.SILENT)
+ tag.attributes.width.set_num(attributes.r.get_num() * 2,
+ Attribute.SyncMode.SILENT)
+ tag.attributes.height.set_num(attributes.r.get_num() * 2,
+ Attribute.SyncMode.SILENT)
+ tag.attributes.rx.set_num(attributes.r.get_num(), Attribute.SyncMode.SILENT)
+ tag.attributes.ry.set_num(attributes.r.get_num(), Attribute.SyncMode.SILENT)
+ "path":
+ tag = TagPath.new()
+ retained_attributes = ["transform", "opacity", "fill", "fill-opacity", "stroke",
+ "stroke-opacity", "stroke-width"]
+ var commands: Array[PathCommand] = []
+ commands.append(PathCommand.MoveCommand.new(attributes.cx.get_num(),
+ attributes.cy.get_num() - attributes.r.get_num(), true))
+ commands.append(PathCommand.EllipticalArcCommand.new(attributes.r.get_num(),
+ attributes.r.get_num(), 0, 0, 0, 0, attributes.r.get_num() * 2, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(attributes.r.get_num(),
+ attributes.r.get_num(), 0, 0, 0, 0, -attributes.r.get_num() * 2, true))
+ commands.append(PathCommand.CloseCommand.new(true))
+ tag.attributes.d.set_commands(commands, Attribute.SyncMode.SILENT)
+ for k in retained_attributes:
+ tag.attributes[k].set_value(attributes[k].get_value(), Attribute.SyncMode.SILENT)
+
+ return tag
diff --git a/src/data_classes/TagEllipse.gd b/src/data_classes/TagEllipse.gd
new file mode 100644
index 000000000..1579040cb
--- /dev/null
+++ b/src/data_classes/TagEllipse.gd
@@ -0,0 +1,77 @@
+## An tag.
+class_name TagEllipse extends Tag
+
+const name = "ellipse"
+const possible_conversions = ["circle", "rect", "path"]
+const icon = preload("res://visual/icons/tag/ellipse.svg")
+
+const known_shape_attributes = ["cx", "cy", "rx", "ry"]
+const known_inheritable_attributes = ["transform", "opacity", "fill", "fill-opacity",
+ "stroke", "stroke-opacity", "stroke-width"]
+
+func _init(pos := Vector2.ZERO) -> void:
+ attributes = {
+ "transform": AttributeTransform.new(),
+ "cx": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "cy": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "rx": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "0", "1"),
+ "ry": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "0", "1"),
+ "opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "fill": AttributeColor.new("black"),
+ "fill-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke": AttributeColor.new("none"),
+ "stroke-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke-width": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "1"),
+ }
+ attributes.cx.set_num(pos.x)
+ attributes.cy.set_num(pos.y)
+ super()
+
+
+func can_replace(new_tag: String) -> bool:
+ if new_tag == "circle":
+ return attributes.rx.get_num() == attributes.ry.get_num()
+ else:
+ return new_tag in ["rect", "path"]
+
+func get_replacement(new_tag: String) -> Tag:
+ if not can_replace(new_tag):
+ return null
+
+ var tag: Tag
+ var retained_attributes: Array[String] = []
+ match new_tag:
+ "circle":
+ tag = TagCircle.new()
+ retained_attributes = ["cx", "cy", "transform", "opacity", "fill",
+ "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]
+ tag.attributes.r.set_num(attributes.rx.get_num(), Attribute.SyncMode.SILENT)
+ "rect":
+ tag = TagRect.new()
+ retained_attributes = ["rx", "ry", "transform", "opacity", "fill",
+ "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]
+ tag.attributes.x.set_num(attributes.cx.get_num() - attributes.rx.get_num(),
+ Attribute.SyncMode.SILENT)
+ tag.attributes.y.set_num(attributes.cy.get_num() - attributes.ry.get_num(),
+ Attribute.SyncMode.SILENT)
+ tag.attributes.width.set_num(attributes.rx.get_num() * 2,
+ Attribute.SyncMode.SILENT)
+ tag.attributes.height.set_num(attributes.ry.get_num() * 2,
+ Attribute.SyncMode.SILENT)
+ "path":
+ tag = TagPath.new()
+ retained_attributes = ["transform", "opacity", "fill",
+ "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]
+ var commands: Array[PathCommand] = []
+ commands.append(PathCommand.MoveCommand.new(attributes.cx.get_num(),
+ attributes.cy.get_num() - attributes.ry.get_num(), true))
+ commands.append(PathCommand.EllipticalArcCommand.new(attributes.rx.get_num(),
+ attributes.ry.get_num(), 0, 0, 0, 0, attributes.ry.get_num() * 2, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(attributes.rx.get_num(),
+ attributes.ry.get_num(), 0, 0, 0, 0, -attributes.ry.get_num() * 2, true))
+ commands.append(PathCommand.CloseCommand.new(true))
+ tag.attributes.d.set_commands(commands, Attribute.SyncMode.SILENT)
+ for k in retained_attributes:
+ tag.attributes[k].set_value(attributes[k].get_value(), Attribute.SyncMode.SILENT)
+
+ return tag
diff --git a/src/data_classes/TagLine.gd b/src/data_classes/TagLine.gd
new file mode 100644
index 000000000..1605bd49a
--- /dev/null
+++ b/src/data_classes/TagLine.gd
@@ -0,0 +1,56 @@
+## A tag.
+class_name TagLine extends Tag
+
+const name = "line"
+const possible_conversions = ["path"]
+const icon = preload("res://visual/icons/tag/line.svg")
+
+const known_shape_attributes = ["x1", "y1", "x2", "y2"]
+const known_inheritable_attributes = ["transform", "opacity", "stroke", "stroke-opacity",
+ "stroke-width", "stroke-linecap"]
+
+func _init(pos := Vector2.ZERO) -> void:
+ attributes = {
+ "transform": AttributeTransform.new(),
+ "x1": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "y1": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "x2": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0", "1"),
+ "y2": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke": AttributeColor.new("none", "#000"),
+ "stroke-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke-width": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "1"),
+ "stroke-linecap": AttributeEnum.new(["butt", "round", "square"], 0),
+ }
+ attributes.x1.set_num(pos.x)
+ attributes.y1.set_num(pos.y)
+ attributes.x2.set_num(pos.x + 1)
+ attributes.y2.set_num(pos.y)
+ super()
+
+
+func can_replace(new_tag: String) -> bool:
+ return new_tag == "path"
+
+func get_replacement(new_tag: String) -> Tag:
+ if not can_replace(new_tag):
+ return null
+
+ var tag: Tag
+ var retained_attributes: Array[String] = []
+ match new_tag:
+ "path":
+ tag = TagPath.new()
+ retained_attributes = ["transform", "opacity", "stroke", "stroke-opacity",
+ "stroke-width", "stroke-linecap"]
+ var commands: Array[PathCommand] = []
+ commands.append(PathCommand.MoveCommand.new(attributes.x1.get_num(),
+ attributes.y1.get_num(), true))
+ commands.append(PathCommand.LineCommand.new(
+ attributes.x2.get_num() - attributes.x1.get_num(),
+ attributes.y2.get_num() - attributes.y1.get_num(), true))
+ tag.attributes.d.set_commands(commands, Attribute.SyncMode.SILENT)
+ for k in retained_attributes:
+ tag.attributes[k].set_value(attributes[k].get_value(), Attribute.SyncMode.SILENT)
+
+ return tag
diff --git a/src/data_classes/TagPath.gd b/src/data_classes/TagPath.gd
new file mode 100644
index 000000000..c37062e16
--- /dev/null
+++ b/src/data_classes/TagPath.gd
@@ -0,0 +1,37 @@
+## A tag.
+class_name TagPath extends Tag
+
+const name = "path"
+const possible_conversions = []
+const icon = preload("res://visual/icons/tag/path.svg")
+
+const known_shape_attributes = ["d"]
+const known_inheritable_attributes = ["transform", "opacity", "fill", "fill-opacity",
+ "stroke", "stroke-opacity", "stroke-width", "stroke-linecap", "stroke-linejoin"]
+
+func _init(pos := Vector2.ZERO) -> void:
+ attributes = {
+ "transform": AttributeTransform.new(),
+ "d": AttributePath.new(),
+ "opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "fill": AttributeColor.new("black"),
+ "fill-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke": AttributeColor.new("none"),
+ "stroke-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke-width": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "1"),
+ "stroke-linecap": AttributeEnum.new(["butt", "round", "square"], 0),
+ "stroke-linejoin": AttributeEnum.new(["miter", "round", "bevel"], 0),
+ }
+ attributes.d.insert_command(0, "M")
+ attributes.d.set_command_property(0, &"x", pos.x)
+ attributes.d.set_command_property(0, &"y", pos.y)
+ super()
+
+func can_replace(_new_tag: String) -> bool:
+ return false
+
+func get_replacement(new_tag: String) -> Tag:
+ if not can_replace(new_tag):
+ return null
+
+ return null
diff --git a/src/data_classes/TagRect.gd b/src/data_classes/TagRect.gd
new file mode 100644
index 000000000..3e0ac2579
--- /dev/null
+++ b/src/data_classes/TagRect.gd
@@ -0,0 +1,121 @@
+## A tag.
+class_name TagRect extends Tag
+
+const name = "rect"
+const possible_conversions = ["circle", "ellipse", "path"]
+const icon = preload("res://visual/icons/tag/rect.svg")
+
+const known_shape_attributes = ["x", "y", "width", "height", "rx", "ry"]
+const known_inheritable_attributes = ["transform", "opacity", "fill", "fill-opacity",
+ "stroke", "stroke-opacity", "stroke-width", "stroke-linejoin"]
+
+func _init(pos := Vector2.ZERO) -> void:
+ attributes = {
+ "transform": AttributeTransform.new(),
+ "x": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "y": AttributeNumeric.new(AttributeNumeric.Mode.FLOAT, "0"),
+ "width": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "0", "1"),
+ "height": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "0", "1"),
+ "rx": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "0"),
+ "ry": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "0"),
+ "opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "fill": AttributeColor.new("black"),
+ "fill-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke": AttributeColor.new("none"),
+ "stroke-opacity": AttributeNumeric.new(AttributeNumeric.Mode.NFLOAT, "1"),
+ "stroke-width": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, "1"),
+ "stroke-linejoin": AttributeEnum.new(["miter", "round", "bevel"], 0),
+ }
+ attributes.x.set_num(pos.x)
+ attributes.y.set_num(pos.y)
+ super()
+
+
+func can_replace(new_tag: String) -> bool:
+ if new_tag == "ellipse":
+ return attributes.rx.get_num() >= attributes.width.get_num() / 2 and\
+ attributes.ry.get_num() >= attributes.height.get_num() / 2
+ elif new_tag == "circle":
+ var side: float = attributes.width.get_num()
+ return attributes.height.get_num() == side and\
+ attributes.rx.get_num() >= side / 2 and attributes.ry.get_num() >= side / 2
+ else:
+ return new_tag == "path"
+
+func get_replacement(new_tag: String) -> Tag:
+ if not can_replace(new_tag):
+ return null
+
+ var tag: Tag
+ var retained_attributes: Array[String] = []
+ match new_tag:
+ "ellipse":
+ tag = TagEllipse.new()
+ retained_attributes = ["transform", "opacity", "fill", "fill-opacity", "stroke",
+ "stroke-opacity", "stroke-width"]
+ tag.attributes.rx.set_num(attributes.width.get_num() / 2,
+ Attribute.SyncMode.SILENT)
+ tag.attributes.ry.set_num(attributes.height.get_num() / 2,
+ Attribute.SyncMode.SILENT)
+ tag.attributes.cx.set_num(attributes.x.get_num() +\
+ attributes.width.get_num() / 2, Attribute.SyncMode.SILENT)
+ tag.attributes.cy.set_num(attributes.y.get_num() +\
+ attributes.height.get_num() / 2, Attribute.SyncMode.SILENT)
+ "circle":
+ tag = TagCircle.new()
+ retained_attributes = ["transform", "opacity", "fill", "fill-opacity",
+ "stroke", "stroke-opacity", "stroke-width"]
+ tag.attributes.r.set_num(attributes.width.get_num() / 2,
+ Attribute.SyncMode.SILENT)
+ tag.attributes.cx.set_num(attributes.x.get_num() +\
+ attributes.width.get_num() / 2, Attribute.SyncMode.SILENT)
+ tag.attributes.cy.set_num(attributes.y.get_num() +\
+ attributes.height.get_num() / 2, Attribute.SyncMode.SILENT)
+ "path":
+ tag = TagPath.new()
+ retained_attributes = ["transform", "opacity", "fill", "fill-opacity", "stroke",
+ "stroke-opacity", "stroke-width", "stroke-linejoin"]
+ var rx := minf(attributes.rx.get_num(), attributes.width.get_num() / 2)
+ var ry := minf(attributes.ry.get_num(), attributes.height.get_num() / 2)
+ var commands: Array[PathCommand] = []
+ if rx == 0 and ry == 0:
+ commands.append(PathCommand.MoveCommand.new(attributes.x.get_num(),
+ attributes.y.get_num(), true))
+ commands.append(PathCommand.HorizontalLineCommand.new(
+ attributes.width.get_num(), true))
+ commands.append(PathCommand.VerticalLineCommand.new(
+ attributes.height.get_num(), true))
+ commands.append(PathCommand.HorizontalLineCommand.new(
+ -attributes.width.get_num(), true))
+ commands.append(PathCommand.CloseCommand.new(true))
+ else:
+ if rx == 0:
+ rx = ry
+ elif ry == 0:
+ ry = rx
+ var w: float = attributes.width.get_num() - rx * 2
+ var h: float = attributes.height.get_num() - ry * 2
+
+ commands.append(PathCommand.MoveCommand.new(attributes.x.get_num(),
+ attributes.y.get_num() + ry, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(
+ rx, ry, 0, 0, 1, rx, -ry, true))
+ if w > 0.0:
+ commands.append(PathCommand.HorizontalLineCommand.new(w, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(
+ rx, ry, 0, 0, 1, rx, ry, true))
+ if h > 0.0:
+ commands.append(PathCommand.VerticalLineCommand.new(h, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(
+ rx, ry, 0, 0, 1, -rx, ry, true))
+ if w > 0.0:
+ commands.append(PathCommand.HorizontalLineCommand.new(-w, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(
+ rx, ry, 0, 0, 1, -rx, -ry, true))
+ commands.append(PathCommand.CloseCommand.new(true))
+ tag.attributes.d.set_commands(commands, Attribute.SyncMode.SILENT)
+
+ for k in retained_attributes:
+ tag.attributes[k].set_value(attributes[k].get_value(), Attribute.SyncMode.SILENT)
+
+ return tag
diff --git a/src/data_classes/TagSVG.gd b/src/data_classes/TagSVG.gd
new file mode 100644
index 000000000..2377ad710
--- /dev/null
+++ b/src/data_classes/TagSVG.gd
@@ -0,0 +1,287 @@
+## A tag.
+class_name TagSVG extends Tag
+
+var width: float
+var height: float
+var viewbox: Rect2
+var canvas_transform: Transform2D
+
+# The difference between attribute_changed() and resized() is that
+# resized() will emit even after unknown changes.
+signal resized
+
+signal child_attribute_changed(undo_redo: bool)
+signal changed_unknown
+
+signal tags_added(tids: Array[PackedInt32Array])
+signal tags_deleted(tids: Array[PackedInt32Array])
+signal tags_moved_in_parent(parent_tid: PackedInt32Array, old_indices: Array[int])
+signal tags_moved_to(tids: Array[PackedInt32Array], location: PackedInt32Array)
+signal tag_changed(tid: PackedInt32Array)
+signal tag_layout_changed # Emitted together with any of the above 5.
+
+# This list is currently only used by the highlighter, so xmlns is here.
+const known_attributes = ["width", "height", "viewBox", "xmlns"]
+const name = "svg"
+
+func _init() -> void:
+ attributes = {
+ "height": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, ""),
+ "width": AttributeNumeric.new(AttributeNumeric.Mode.UFLOAT, ""),
+ "viewBox": AttributeList.new(),
+ }
+ unknown_attributes.append(AttributeUnknown.new("xmlns", "http://www.w3.org/2000/svg"))
+ attribute_changed.connect(update_cache.unbind(1))
+ changed_unknown.connect(update_cache)
+ update_cache()
+ super()
+
+func update_cache() -> void:
+ # Cache width.
+ if is_finite(attributes.width.get_num()):
+ width = attributes.width.get_num()
+ else:
+ width = attributes.viewBox.get_list_element(2)
+ # Cache height.
+ if is_finite(attributes.height.get_num()):
+ height = attributes.height.get_num()
+ else:
+ height = attributes.viewBox.get_list_element(3)
+ # Cache viewbox.
+ if attributes.viewBox.get_list_size() >= 4:
+ viewbox = Rect2(attributes.viewBox.get_list_element(0),
+ attributes.viewBox.get_list_element(1),
+ attributes.viewBox.get_list_element(2),
+ attributes.viewBox.get_list_element(3))
+ else:
+ if is_finite(attributes.width.get_num()) and is_finite(attributes.height.get_num()):
+ viewbox = Rect2(0, 0, attributes.width.get_num(), attributes.height.get_num())
+ else:
+ viewbox = Rect2(0, 0, 0, 0)
+ # Cache canvas transform.
+ var width_ratio := width / viewbox.size.x
+ var height_ratio := height / viewbox.size.y
+ if width_ratio < height_ratio:
+ canvas_transform = Transform2D(0.0, Vector2(width_ratio, width_ratio), 0.0,
+ -viewbox.position * width_ratio +\
+ Vector2(0, (height - width_ratio * viewbox.size.y) / 2))
+ else:
+ canvas_transform = Transform2D(0.0, Vector2(height_ratio, height_ratio), 0.0,
+ -viewbox.position * height_ratio +\
+ Vector2((width - height_ratio * viewbox.size.x) / 2, 0))
+
+
+func canvas_to_world(pos: Vector2) -> Vector2:
+ return canvas_transform * pos
+
+func world_to_canvas(pos: Vector2) -> Vector2:
+ return canvas_transform.affine_inverse() * pos
+
+
+func get_size() -> Vector2:
+ return Vector2(width, height)
+
+
+func get_all_tids() -> Array[PackedInt32Array]:
+ var tids: Array[PackedInt32Array] = []
+ var unchecked_tids: Array[PackedInt32Array] = []
+ for idx in get_child_count():
+ unchecked_tids.append(PackedInt32Array([idx]))
+
+ while not unchecked_tids.is_empty():
+ var checked_tid: PackedInt32Array = unchecked_tids.pop_back()
+ var checked_tag: Tag = get_tag(checked_tid)
+ for idx in checked_tag.get_child_count():
+ var new_tid := checked_tid.duplicate()
+ new_tid.append(idx)
+ unchecked_tids.append(new_tid)
+ tids.append(checked_tid)
+ return tids
+
+func get_tag(tid: PackedInt32Array) -> Tag:
+ var current_tag: Tag = self
+ for idx in tid:
+ if idx >= current_tag.child_tags.size():
+ return null
+ current_tag = current_tag.child_tags[idx]
+ return current_tag
+
+
+func add_tag(new_tag: Tag, new_tid: PackedInt32Array) -> void:
+ var parent_tid := Utils.get_parent_tid(new_tid)
+ get_tag(parent_tid).child_tags.insert(new_tid[-1], new_tag)
+ new_tag.attribute_changed.connect(emit_child_attribute_changed)
+ var new_tid_array: Array[PackedInt32Array] = [new_tid]
+ tags_added.emit(new_tid_array)
+ tag_layout_changed.emit()
+
+func replace_self(new_tag: Tag) -> void:
+ var old_size := get_size()
+ for attrib in attributes:
+ attributes[attrib].set_value(new_tag.attributes[attrib].get_value(),
+ Attribute.SyncMode.SILENT)
+
+ unknown_attributes.clear()
+ for attrib in new_tag.unknown_attributes:
+ unknown_attributes.append(attrib)
+ child_tags.clear()
+
+ for tag in new_tag.child_tags:
+ child_tags.append(tag)
+
+ for tid in get_all_tids():
+ get_tag(tid).attribute_changed.connect(emit_child_attribute_changed)
+
+ changed_unknown.emit()
+ if old_size != get_size():
+ resized.emit()
+
+func delete_tags(tids: Array[PackedInt32Array]) -> void:
+ if tids.is_empty():
+ return
+
+ tids = Utils.filter_descendant_tids(tids)
+ for tid in tids:
+ var parent_tid := Utils.get_parent_tid(tid)
+ var parent_tag := get_tag(parent_tid)
+ if parent_tag != null:
+ var tag_idx := tid[-1]
+ if tag_idx < parent_tag.get_child_count():
+ parent_tag.child_tags.remove_at(tag_idx)
+ tags_deleted.emit(tids)
+ tag_layout_changed.emit()
+
+# Moves tags up or down, not to an arbitrary position.
+func move_tags_in_parent(tids: Array[PackedInt32Array], down: bool) -> void:
+ if tids.is_empty():
+ return
+
+ # For moving, all these tags must be direct children of the same parent.
+ tids = Utils.filter_descendant_tids(tids)
+ var depth := tids[0].size()
+ var parent_tid := Utils.get_parent_tid(tids[0])
+ for tid in tids:
+ if tid.size() != depth or Utils.get_parent_tid(tid) != parent_tid:
+ return
+
+ var tid_indices: Array[int] = [] # The last indices of the TIDs.
+ for tid in tids:
+ tid_indices.append(tid[-1])
+
+ var parent_tag := get_tag(parent_tid)
+ var parent_child_count := parent_tag.get_child_count()
+ var old_indices: Array[int] = []
+ for i in parent_child_count:
+ old_indices.append(i)
+ # Do the moving.
+ if down:
+ var i := parent_child_count - 1
+ while i >= 0:
+ if not i in tid_indices and (i - 1) in tid_indices:
+ old_indices.remove_at(i)
+ var moved_i := i
+ var moved_tag: Tag = parent_tag.child_tags.pop_at(i)
+ while (i - 1) in tid_indices:
+ i -= 1
+ old_indices.insert(i, moved_i)
+ parent_tag.child_tags.insert(i, moved_tag)
+ i -= 1
+ else:
+ var i := 0
+ while i < parent_child_count:
+ if not i in tid_indices and (i + 1) in tid_indices:
+ old_indices.remove_at(i)
+ var moved_i := i
+ var moved_tag: Tag = parent_tag.child_tags.pop_at(i)
+ while (i + 1) in tid_indices:
+ i += 1
+ old_indices.insert(i, moved_i)
+ parent_tag.child_tags.insert(i, moved_tag)
+ i += 1
+ # Check if indices were really changed after the operation.
+ if old_indices != range(old_indices.size()):
+ tags_moved_in_parent.emit(parent_tid, old_indices)
+ tag_layout_changed.emit()
+
+# Moves tags to an arbitrary position. The first moved tag will move to the location TID.
+func move_tags_to(tids: Array[PackedInt32Array], location: PackedInt32Array) -> void:
+ tids = Utils.filter_descendant_tids(tids)
+ # A tag can't move deeper inside itself. Remove the descendants of the location.
+ for i in range(tids.size() - 1, -1, -1):
+ if Utils.is_tid_parent(tids[i], location):
+ tids.remove_at(i)
+
+ # Remove tags from their old locations.
+ var tids_stored: Array[PackedInt32Array] = []
+ var tags_stored: Array[Tag] = []
+ for tid in tids:
+ # Shift the new location if tags before it were removed. A tag is "before"
+ # if it has the same parent as the new location, but is before that location.
+ if tid.size() <= location.size():
+ var before := true
+ for i in tid.size() - 1:
+ if tid[i] != location[i]:
+ before = false
+ break
+ if before and tid[-1] < location[tid.size() - 1]:
+ location[tid.size() - 1] -= 1
+ tids_stored.append(tid)
+ tags_stored.append(get_tag(Utils.get_parent_tid(tid)).child_tags.pop_at(tid[-1]))
+ # Add the tags back in the new location.
+ for tag in tags_stored:
+ get_tag(Utils.get_parent_tid(location)).child_tags.insert(location[-1], tag)
+ # Check if this actually chagned the layout.
+ for tid in tids_stored:
+ if not Utils.are_tid_parents_same(tid, location) or tid[-1] < location[-1] or\
+ tid[-1] >= location[-1] + tids_stored.size():
+ # If this condition is passed, then there was a layout change.
+ tags_moved_to.emit(tids, location)
+ tag_layout_changed.emit()
+ return
+
+# Duplicates tags and puts them below.
+func duplicate_tags(tids: Array[PackedInt32Array]) -> void:
+ if tids.is_empty():
+ return
+
+ tids = Utils.filter_descendant_tids(tids)
+ var tids_added: Array[PackedInt32Array] = []
+ # Used to offset previously added TIDs in tids_added after duplicating a tag before.
+ var last_parent := PackedInt32Array([-1]) # Start with a TID that can't be matched.
+ var added_to_last_parent := 0
+
+ for tid in tids:
+ var new_tag := get_tag(tid).create_duplicate()
+ # Add the new tag.
+ var new_tid := tid.duplicate()
+ new_tid[-1] += 1
+ var parent_tid := Utils.get_parent_tid(new_tid)
+ get_tag(parent_tid).child_tags.insert(new_tid[-1], new_tag)
+ new_tag.attribute_changed.connect(emit_child_attribute_changed)
+ # Add the TID and offset the other ones from the same parent.
+ var added_tid_idx := tids_added.size()
+ tids_added.append(new_tid)
+ if last_parent == parent_tid:
+ added_to_last_parent += 1
+ else:
+ last_parent = parent_tid
+ added_to_last_parent = 0
+ for tid_idx in range(added_tid_idx - added_to_last_parent , added_tid_idx):
+ tids_added[tid_idx][-1] += 1
+ tags_added.emit(tids_added)
+ tag_layout_changed.emit()
+
+func replace_tag(tid: PackedInt32Array, new_tag: Tag) -> void:
+ if tid.is_empty():
+ replace_self(new_tag)
+ get_tag(Utils.get_parent_tid(tid)).child_tags[tid[-1]] = new_tag
+ new_tag.attribute_changed.connect(emit_child_attribute_changed)
+ tag_changed.emit(tid)
+ tag_layout_changed.emit()
+
+func emit_child_attribute_changed(undo_redo: bool) -> void:
+ child_attribute_changed.emit(undo_redo)
+
+func emit_attribute_changed(undo_redo: bool) -> void:
+ super(undo_redo)
+ resized.emit()
diff --git a/src/data_classes/TagUnknown.gd b/src/data_classes/TagUnknown.gd
new file mode 100644
index 000000000..abc4e1cad
--- /dev/null
+++ b/src/data_classes/TagUnknown.gd
@@ -0,0 +1,11 @@
+## A tag not recognized by GodSVG.
+class_name TagUnknown extends Tag
+
+var name: String
+const possible_conversions = []
+const known_shape_attributes = []
+const known_inheritable_attributes = []
+const icon = preload("res://visual/icons/tag/unknown.svg")
+
+func _init(new_name: String) -> void:
+ name = new_name
diff --git a/src/interface_elements/Display.gd b/src/interface_elements/Display.gd
deleted file mode 100644
index b5c107777..000000000
--- a/src/interface_elements/Display.gd
+++ /dev/null
@@ -1,30 +0,0 @@
-extends VBoxContainer
-
-@onready var viewport: Viewport = $ViewportContainer/Viewport
-@onready var zoom_reset_button: Button = %ZoomReset
-@onready var display: TextureRect = %Checkerboard
-
-var zoom_level := 1.0:
- set(value):
- zoom_level = clampf(value, 0.25, 4.0)
- zoom_reset_button.text = "%d%%" % (zoom_level * 100)
- viewport.size_2d_override = viewport.size / zoom_level
- clamp_view()
-
-func _on_zoom_out_pressed() -> void:
- zoom_level /= 2
-
-func _on_zoom_in_pressed() -> void:
- zoom_level *= 2
-
-func _on_zoom_reset_pressed() -> void:
- zoom_level = 1.0
-
-func _input(event: InputEvent) -> void:
- if event is InputEventMouseMotion and event.button_mask == MOUSE_BUTTON_MASK_LEFT:
- display.position += event.relative / zoom_level
- clamp_view()
-
-func clamp_view() -> void:
- display.position = display.position.clamp(
- -viewport.size / 2, viewport.size / zoom_level - Vector2(16, 16))
diff --git a/src/interface_elements/color_field.gd b/src/interface_elements/color_field.gd
deleted file mode 100644
index aad856484..000000000
--- a/src/interface_elements/color_field.gd
+++ /dev/null
@@ -1,76 +0,0 @@
-extends HBoxContainer
-
-@onready var color_button: Button = $Button
-@onready var color_edit: LineEdit = $LineEdit
-@onready var color_picker: Popup = $ColorPopup
-
-var attribute: SVGAttribute
-
-signal value_changed(new_value: String)
-var value: String:
- set(new_value):
- var old_value := value
- value = validate(new_value, old_value)
- if value != old_value:
- value_changed.emit(new_value)
-
-func _ready() -> void:
- value_changed.connect(_on_value_changed)
- if attribute != null:
- value = attribute.value
- color_edit.text = value
-
-func validate(new_value: String, old_value: String) -> String:
- if new_value == "none" or (new_value.is_valid_html_color() and\
- not new_value.begins_with("#")):
- return new_value
- else:
- return old_value
-
-func _on_value_changed(new_value: String) -> void:
- color_edit.text = new_value
- queue_redraw()
- if attribute != null:
- attribute.value = new_value
- SVG.update()
-
-func _on_button_pressed() -> void:
- color_picker.popup(Rect2(color_edit.global_position + Vector2(0, color_edit.size.y),
- color_picker.size))
-
-func _draw() -> void:
- var button_size := color_button.get_size()
- var line_edit_size := color_edit.get_size()
- var col := Color.from_string(value, Color(0, 0, 0, 0))
- draw_rect(Rect2(Vector2(line_edit_size.x, 1), button_size - Vector2(5, 2)), col)
- draw_rect(Rect2(Vector2(line_edit_size.x + button_size.x - 5, 5), Vector2(3, 12)), col)
- draw_circle(Vector2(line_edit_size.x + button_size.x - 8, 7), 6, col)
- draw_circle(Vector2(line_edit_size.x + button_size.x - 8, 15), 6, col)
-
-
-# Hacks to make LineEdit bearable.
-
-func _on_focus_entered() -> void:
- get_tree().paused = true
-
-func _on_focus_exited() -> void:
- value = color_edit.text
- get_tree().paused = false
-
-func _on_text_submitted(new_text: String) -> void:
- value = new_text
- color_edit.release_focus()
-
-func _input(event: InputEvent) -> void:
- if (color_edit.has_focus() and event is InputEventMouseButton and\
- not color_edit.get_global_rect().has_point(event.position)):
- color_edit.release_focus()
-
-
-func _on_text_changed(new_text: String) -> void:
- # TODO
- if new_text == "#":
- color_edit.delete_char_at_caret()
-
-func _on_color_picked(new_color: String) -> void:
- value = new_color
diff --git a/src/interface_elements/color_field.tscn b/src/interface_elements/color_field.tscn
deleted file mode 100644
index 262b1402b..000000000
--- a/src/interface_elements/color_field.tscn
+++ /dev/null
@@ -1,281 +0,0 @@
-[gd_scene load_steps=8 format=3 uid="uid://carf2o1y7wvmc"]
-
-[ext_resource type="Script" path="res://src/interface_elements/color_field.gd" id="1_330tl"]
-[ext_resource type="Script" path="res://src/interface_elements/color_popup.gd" id="2_hgyyv"]
-[ext_resource type="PackedScene" uid="uid://cpvtf3kaa2ltr" path="res://src/interface_elements/color_swatch.tscn" id="3_38vpq"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fpguh"]
-content_margin_left = 3.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 0.0
-bg_color = Color(0, 0, 0, 0)
-border_width_left = 1
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.0588235, 0.117647, 0.301961, 1)
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_r5acu"]
-content_margin_left = 3.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 0.0
-bg_color = Color(0, 0, 0, 0)
-border_width_left = 1
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.0745098, 0.156863, 0.4, 1)
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5n6p7"]
-content_margin_left = 3.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 0.0
-bg_color = Color(0, 0, 0, 0)
-border_width_left = 1
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.6, 0.8, 1, 1)
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_16ktf"]
-content_margin_left = 3.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 0.0
-bg_color = Color(0, 0, 0, 0)
-border_width_left = 1
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.121569, 0.121569, 0.14902, 1)
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_detail = 16
-
-[node name="ColorField" type="HBoxContainer"]
-custom_minimum_size = Vector2(0, 22)
-offset_right = 50.0
-offset_bottom = 21.0
-theme_override_constants/separation = 0
-script = ExtResource("1_330tl")
-
-[node name="LineEdit" type="LineEdit" parent="."]
-custom_minimum_size = Vector2(54, 0)
-layout_mode = 2
-max_length = 8
-context_menu_enabled = false
-select_all_on_focus = true
-caret_blink = true
-
-[node name="Button" type="Button" parent="."]
-custom_minimum_size = Vector2(14, 0)
-layout_mode = 2
-mouse_default_cursor_shape = 2
-theme_override_styles/normal = SubResource("StyleBoxFlat_fpguh")
-theme_override_styles/hover = SubResource("StyleBoxFlat_r5acu")
-theme_override_styles/pressed = SubResource("StyleBoxFlat_5n6p7")
-theme_override_styles/disabled = SubResource("StyleBoxFlat_16ktf")
-
-[node name="ColorPopup" type="Popup" parent="."]
-size = Vector2i(150, 142)
-script = ExtResource("2_hgyyv")
-
-[node name="PanelContainer" type="PanelContainer" parent="ColorPopup"]
-custom_minimum_size = Vector2(150, 0)
-offset_right = 150.0
-offset_bottom = 142.0
-
-[node name="MarginContainer" type="MarginContainer" parent="ColorPopup/PanelContainer"]
-layout_mode = 2
-theme_override_constants/margin_left = 8
-theme_override_constants/margin_top = 0
-theme_override_constants/margin_right = 8
-theme_override_constants/margin_bottom = 8
-
-[node name="MainContainer" type="VBoxContainer" parent="ColorPopup/PanelContainer/MarginContainer"]
-layout_mode = 2
-theme_override_constants/separation = 8
-
-[node name="Pure" type="VBoxContainer" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer"]
-layout_mode = 2
-theme_override_constants/separation = 0
-
-[node name="Label" type="Label" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Pure"]
-layout_mode = 2
-text = "Pure"
-horizontal_alignment = 1
-vertical_alignment = 1
-
-[node name="PureSwatches" type="HFlowContainer" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Pure"]
-unique_name_in_owner = true
-layout_mode = 2
-theme_override_constants/h_separation = 2
-
-[node name="White" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Pure/PureSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Pure white"
-color_hex = "ffffff"
-
-[node name="Black" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Pure/PureSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Pure black"
-color_hex = "000000"
-
-[node name="Red" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Pure/PureSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Pure red"
-color_hex = "ff0000"
-
-[node name="Green" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Pure/PureSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Pure green"
-color_hex = "00ff00"
-
-[node name="Blue" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Pure/PureSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Pure blue"
-color_hex = "0000ff"
-
-[node name="Blue2" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Pure/PureSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Pure blue"
-color_hex = "none"
-
-[node name="Common" type="VBoxContainer" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer"]
-layout_mode = 2
-theme_override_constants/separation = 0
-
-[node name="Label" type="Label" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common"]
-layout_mode = 2
-text = "Common"
-horizontal_alignment = 1
-vertical_alignment = 1
-
-[node name="CommonSwatches" type="HFlowContainer" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common"]
-unique_name_in_owner = true
-layout_mode = 2
-theme_override_constants/h_separation = 2
-
-[node name="Icon" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Icon color"
-color_hex = "e0e0e0"
-
-[node name="IconDisabled" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Icon disabled color"
-color_hex = "919191"
-
-[node name="Node2D" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Node2D color"
-color_hex = "8da5f3"
-
-[node name="Node2DDark" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Node2D dark color"
-color_hex = "4b70ea"
-
-[node name="Control" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Control color"
-color_hex = "8eef97"
-
-[node name="Node3D" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Node3D color"
-color_hex = "fc7f7f"
-
-[node name="Animation" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Animation color"
-color_hex = "c38ef1"
-
-[node name="Mesh" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Mesh color"
-color_hex = "ffca5f"
-
-[node name="Shape" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Shape color"
-color_hex = "2998ff"
-
-[node name="ShapeLight" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Shape light color"
-color_hex = "a2d2ff"
-
-[node name="Input" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Common/CommonSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Input color"
-color_hex = "69c4d4"
-
-[node name="Rainbow" type="VBoxContainer" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer"]
-layout_mode = 2
-theme_override_constants/separation = 0
-
-[node name="Label" type="Label" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow"]
-layout_mode = 2
-text = "Rainbow"
-horizontal_alignment = 1
-vertical_alignment = 1
-
-[node name="RainbowSwatches" type="HFlowContainer" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow"]
-unique_name_in_owner = true
-layout_mode = 2
-theme_override_constants/h_separation = 2
-
-[node name="Red" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow/RainbowSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Rainbow red"
-color_hex = "ff4545"
-
-[node name="Yellow" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow/RainbowSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Rainbow yellow"
-color_hex = "ffe345"
-
-[node name="Green" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow/RainbowSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Rainbow green"
-color_hex = "80ff45"
-
-[node name="Aqua" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow/RainbowSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Rainbow aqua"
-color_hex = "45ffa2"
-
-[node name="Blue" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow/RainbowSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Rainbow blue"
-color_hex = "45d7ff"
-
-[node name="Purple" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow/RainbowSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Rainbow purple"
-color_hex = "8045ff"
-
-[node name="Pink" parent="ColorPopup/PanelContainer/MarginContainer/MainContainer/Rainbow/RainbowSwatches" instance=ExtResource("3_38vpq")]
-layout_mode = 2
-tooltip_text = "Rainbow pink"
-color_hex = "ff4596"
-
-[connection signal="focus_entered" from="LineEdit" to="." method="_on_focus_entered"]
-[connection signal="focus_exited" from="LineEdit" to="." method="_on_focus_exited"]
-[connection signal="text_changed" from="LineEdit" to="." method="_on_text_changed"]
-[connection signal="text_submitted" from="LineEdit" to="." method="_on_text_submitted"]
-[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
-[connection signal="color_picked" from="ColorPopup" to="." method="_on_color_picked"]
diff --git a/src/interface_elements/color_popup.gd b/src/interface_elements/color_popup.gd
deleted file mode 100644
index 90f59ee67..000000000
--- a/src/interface_elements/color_popup.gd
+++ /dev/null
@@ -1,20 +0,0 @@
-extends Popup
-
-signal color_picked(new_color: String)
-
-@onready var swatch_containers: Array[Node] =\
- [%PureSwatches, %CommonSwatches, %RainbowSwatches]
-
-func _ready() -> void:
- for swatch_container in swatch_containers:
- for swatch in swatch_container.get_children():
- swatch.gui_input.connect(_on_gui_input)
-
-func _on_gui_input(event: InputEvent) -> void:
- if event is InputEventMouseButton and event.is_pressed() and\
- event.button_mask == MOUSE_BUTTON_LEFT:
- for swatch_container in swatch_containers:
- for swatch in swatch_container.get_children():
- if swatch.get_global_rect().has_point(get_mouse_position()):
- color_picked.emit(swatch.color_hex)
- hide()
diff --git a/src/interface_elements/color_swatch.gd b/src/interface_elements/color_swatch.gd
deleted file mode 100644
index 19183e2fe..000000000
--- a/src/interface_elements/color_swatch.gd
+++ /dev/null
@@ -1,10 +0,0 @@
-extends PanelContainer
-
-@export var color_hex := ""
-
-@onready var color_rect: ColorRect = $ColorRect
-
-func _ready() -> void:
- if color_hex == "none":
- color_rect.queue_free()
- color_rect.color = Color.from_string(color_hex, Color(0, 0, 0))
diff --git a/src/interface_elements/color_swatch.tscn b/src/interface_elements/color_swatch.tscn
deleted file mode 100644
index ea15fcfff..000000000
--- a/src/interface_elements/color_swatch.tscn
+++ /dev/null
@@ -1,36 +0,0 @@
-[gd_scene load_steps=4 format=3 uid="uid://cpvtf3kaa2ltr"]
-
-[ext_resource type="Script" path="res://src/interface_elements/color_swatch.gd" id="1_2b2yq"]
-[ext_resource type="Texture2D" uid="uid://c68og6bsqt0lb" path="res://visual/Checkerboard.svg" id="2_f4i7i"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i5pvy"]
-bg_color = Color(0, 0, 0, 0)
-border_width_left = 2
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.866667, 0.933333, 1, 1)
-corner_radius_top_left = 1
-corner_radius_top_right = 1
-corner_radius_bottom_right = 1
-corner_radius_bottom_left = 1
-corner_detail = 2
-
-[node name="ColorSwatch" type="PanelContainer"]
-mouse_default_cursor_shape = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_i5pvy")
-script = ExtResource("1_2b2yq")
-color_hex = null
-
-[node name="TextureRect" type="TextureRect" parent="."]
-custom_minimum_size = Vector2(16, 16)
-layout_mode = 2
-mouse_filter = 2
-texture = ExtResource("2_f4i7i")
-expand_mode = 1
-
-[node name="ColorRect" type="ColorRect" parent="."]
-custom_minimum_size = Vector2(16, 16)
-layout_mode = 2
-size_flags_horizontal = 0
-mouse_filter = 2
diff --git a/src/interface_elements/interface.gd b/src/interface_elements/interface.gd
deleted file mode 100644
index e971472f6..000000000
--- a/src/interface_elements/interface.gd
+++ /dev/null
@@ -1,46 +0,0 @@
-extends VBoxContainer
-
-const TagEditor = preload("tag_editor.tscn")
-
-@onready var shapes: VBoxContainer = $Shapes
-
-func add_circle() -> void:
- var circle_editor := TagEditor.instantiate()
- circle_editor.tag_index = SVG.data.tags.size()
- var circle := SVGTagCircle.new()
- for attribute in circle.attributes:
- match attribute:
- "r": circle.attributes[attribute].value = 1.0
- _: circle.attributes[attribute].value = circle.attributes[attribute].default
- circle_editor.tag = circle
- shapes.add_child(circle_editor)
-
-func add_ellipse() -> void:
- var ellipse_editor := TagEditor.instantiate()
- ellipse_editor.tag_index = SVG.data.tags.size()
- var ellipse := SVGTagEllipse.new()
- for attribute in ellipse.attributes:
- match attribute:
- "rx": ellipse.attributes[attribute].value = 1.0
- "ry": ellipse.attributes[attribute].value = 1.0
- _: ellipse.attributes[attribute].value = ellipse.attributes[attribute].default
- ellipse_editor.tag = ellipse
- shapes.add_child(ellipse_editor)
-
-func add_rect() -> void:
- var rect_editor := TagEditor.instantiate()
- rect_editor.tag_index = SVG.data.tags.size()
- var rect := SVGTagRect.new()
- for attribute in rect.attributes:
- match attribute:
- "width": rect.attributes[attribute].value = 1.0
- "height": rect.attributes[attribute].value = 1.0
- _: rect.attributes[attribute].value = rect.attributes[attribute].default
- rect_editor.tag = rect
- shapes.add_child(rect_editor)
-
-func _change_view_box(w: int, h: int) -> void:
- SVG.data.w = w
- SVG.data.h = h
- SVG.update()
- %Checkerboard.custom_minimum_size = Vector2(w/float(h), h/float(w)) * 320
diff --git a/src/interface_elements/interface.tscn b/src/interface_elements/interface.tscn
deleted file mode 100644
index f9c44fc37..000000000
--- a/src/interface_elements/interface.tscn
+++ /dev/null
@@ -1,60 +0,0 @@
-[gd_scene load_steps=8 format=3 uid="uid://ccynisiuyn5qn"]
-
-[ext_resource type="Script" path="res://src/interface_elements/interface.gd" id="1_16ggy"]
-[ext_resource type="Script" path="res://src/interface_elements/view_box_edit.gd" id="2_osq0a"]
-[ext_resource type="FontFile" uid="uid://c7ury252fql35" path="res://visual/CodeFont.ttf" id="3_8oiup"]
-[ext_resource type="PackedScene" uid="uid://bp2vpf7g8w8aj" path="res://src/interface_elements/number_field.tscn" id="4_6tphi"]
-[ext_resource type="Texture2D" uid="uid://dmnaqiwq4de1j" path="res://visual/icons/Circle.svg" id="5_cwjdk"]
-[ext_resource type="Texture2D" uid="uid://c8t8eiojuwjje" path="res://visual/icons/Ellipse.svg" id="6_komru"]
-[ext_resource type="Texture2D" uid="uid://cso2l5nvm6gm" path="res://visual/icons/Rect.svg" id="7_hdyp6"]
-
-[node name="Interface" type="VBoxContainer"]
-size_flags_horizontal = 3
-theme_override_constants/separation = 16
-script = ExtResource("1_16ggy")
-
-[node name="MainConfiguration" type="VBoxContainer" parent="."]
-layout_mode = 2
-
-[node name="ViewBoxEdit" type="HBoxContainer" parent="MainConfiguration"]
-layout_mode = 2
-script = ExtResource("2_osq0a")
-
-[node name="Label" type="Label" parent="MainConfiguration/ViewBoxEdit"]
-layout_mode = 2
-theme_override_fonts/font = ExtResource("3_8oiup")
-theme_override_font_sizes/font_size = 14
-text = "viewBox"
-
-[node name="WidthEdit" parent="MainConfiguration/ViewBoxEdit" instance=ExtResource("4_6tphi")]
-layout_mode = 2
-
-[node name="HeightEdit" parent="MainConfiguration/ViewBoxEdit" instance=ExtResource("4_6tphi")]
-layout_mode = 2
-
-[node name="Shapes" type="VBoxContainer" parent="."]
-layout_mode = 2
-
-[node name="HBoxContainer" type="HBoxContainer" parent="Shapes"]
-layout_mode = 2
-
-[node name="Circle" type="Button" parent="Shapes/HBoxContainer"]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-icon = ExtResource("5_cwjdk")
-icon_alignment = 1
-
-[node name="Ellipse" type="Button" parent="Shapes/HBoxContainer"]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-icon = ExtResource("6_komru")
-
-[node name="Rect" type="Button" parent="Shapes/HBoxContainer"]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-icon = ExtResource("7_hdyp6")
-
-[connection signal="view_box_changed" from="MainConfiguration/ViewBoxEdit" to="." method="_change_view_box"]
-[connection signal="pressed" from="Shapes/HBoxContainer/Circle" to="." method="add_circle"]
-[connection signal="pressed" from="Shapes/HBoxContainer/Ellipse" to="." method="add_ellipse"]
-[connection signal="pressed" from="Shapes/HBoxContainer/Rect" to="." method="add_rect"]
diff --git a/src/interface_elements/main_scene.tscn b/src/interface_elements/main_scene.tscn
deleted file mode 100644
index 016f8862a..000000000
--- a/src/interface_elements/main_scene.tscn
+++ /dev/null
@@ -1,172 +0,0 @@
-[gd_scene load_steps=7 format=3 uid="uid://ce6j54x27pom"]
-
-[ext_resource type="PackedScene" uid="uid://ccynisiuyn5qn" path="res://src/interface_elements/interface.tscn" id="1_afxvd"]
-[ext_resource type="Script" path="res://src/interface_elements/Display.gd" id="2_ylpv1"]
-[ext_resource type="Texture2D" uid="uid://c68og6bsqt0lb" path="res://visual/Checkerboard.svg" id="3_d58qh"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_082e3"]
-bg_color = Color(0.005, 0.005, 0.05, 1)
-border_width_right = 2
-border_color = Color(0.4, 0.7, 1, 1)
-
-[sub_resource type="CodeHighlighter" id="CodeHighlighter_jg7xq"]
-number_color = Color(0.631373, 1, 0.878431, 1)
-symbol_color = Color(0.670588, 0.788235, 1, 1)
-function_color = Color(0.341176, 0.701961, 1, 1)
-keyword_colors = {
-"circle": Color(1, 0.54902, 0.8, 1),
-"cx": Color(0.737255, 0.878431, 1, 1),
-"cy": Color(0.737255, 0.878431, 1, 1),
-"ellipse": Color(1, 0.54902, 0.8, 1),
-"fill": Color(0.737255, 0.878431, 1, 1),
-"g": Color(1, 0.54902, 0.8, 1),
-"height": Color(0.737255, 0.878431, 1, 1),
-"path": Color(1, 0.54902, 0.8, 1),
-"rect": Color(1, 0.54902, 0.8, 1),
-"rx": Color(0.737255, 0.878431, 1, 1),
-"ry": Color(0.737255, 0.878431, 1, 1),
-"stroke": Color(0.737255, 0.878431, 1, 1),
-"stroke-width": Color(0.737255, 0.878431, 1, 1),
-"svg": Color(1, 0.54902, 0.8, 1),
-"viewBox": Color(0.737255, 0.878431, 1, 1),
-"width": Color(0.737255, 0.878431, 1, 1),
-"x": Color(0.737255, 0.878431, 1, 1),
-"y": Color(0.737255, 0.878431, 1, 1)
-}
-color_regions = {
-"\" \"": Color(0.631373, 1, 0.878431, 1)
-}
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nxvgo"]
-bg_color = Color(0.005, 0.005, 0.05, 1)
-border_width_bottom = 2
-border_color = Color(0.4, 0.7, 1, 1)
-
-[node name="MainScene" type="HBoxContainer"]
-process_mode = 3
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_constants/separation = 0
-
-[node name="PanelContainer" type="PanelContainer" parent="."]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_styles/panel = SubResource("StyleBoxFlat_082e3")
-
-[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_constants/margin_left = 8
-theme_override_constants/margin_top = 8
-theme_override_constants/margin_right = 8
-theme_override_constants/margin_bottom = 8
-
-[node name="MainContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
-layout_mode = 2
-theme_override_constants/separation = 8
-
-[node name="CodeEdit" type="CodeEdit" parent="PanelContainer/MarginContainer/MainContainer"]
-unique_name_in_owner = true
-custom_minimum_size = Vector2(0, 96)
-layout_mode = 2
-editable = false
-wrap_mode = 1
-highlight_all_occurrences = true
-syntax_highlighter = SubResource("CodeHighlighter_jg7xq")
-scroll_smooth = true
-scroll_v_scroll_speed = 30.0
-caret_blink = true
-caret_multiple = false
-auto_brace_completion_highlight_matching = true
-auto_brace_completion_pairs = {
-"\"": "\"",
-"(": ")",
-"<": ">"
-}
-
-[node name="Interface" parent="PanelContainer/MarginContainer/MainContainer" instance=ExtResource("1_afxvd")]
-layout_mode = 2
-
-[node name="Display" type="VBoxContainer" parent="."]
-layout_mode = 2
-theme_override_constants/separation = 0
-script = ExtResource("2_ylpv1")
-
-[node name="PanelContainer" type="PanelContainer" parent="Display"]
-layout_mode = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_nxvgo")
-
-[node name="MarginContainer" type="MarginContainer" parent="Display/PanelContainer"]
-layout_mode = 2
-theme_override_constants/margin_left = 4
-theme_override_constants/margin_top = 4
-theme_override_constants/margin_right = 4
-theme_override_constants/margin_bottom = 4
-
-[node name="HBoxContainer" type="HBoxContainer" parent="Display/PanelContainer/MarginContainer"]
-layout_mode = 2
-alignment = 2
-
-[node name="HBoxContainer" type="HBoxContainer" parent="Display/PanelContainer/MarginContainer/HBoxContainer"]
-layout_mode = 2
-alignment = 1
-
-[node name="ZoomOut" type="Button" parent="Display/PanelContainer/MarginContainer/HBoxContainer/HBoxContainer"]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-text = " - "
-
-[node name="ZoomReset" type="Button" parent="Display/PanelContainer/MarginContainer/HBoxContainer/HBoxContainer"]
-unique_name_in_owner = true
-custom_minimum_size = Vector2(56, 0)
-layout_mode = 2
-mouse_default_cursor_shape = 2
-text = "100%"
-
-[node name="ZoomIn" type="Button" parent="Display/PanelContainer/MarginContainer/HBoxContainer/HBoxContainer"]
-layout_mode = 2
-mouse_default_cursor_shape = 2
-text = " + "
-
-[node name="ViewportContainer" type="SubViewportContainer" parent="Display"]
-layout_mode = 2
-size_flags_horizontal = 10
-
-[node name="Viewport" type="SubViewport" parent="Display/ViewportContainer"]
-handle_input_locally = false
-size = Vector2i(600, 600)
-size_2d_override_stretch = true
-render_target_update_mode = 4
-
-[node name="Checkerboard" type="TextureRect" parent="Display/ViewportContainer/Viewport"]
-unique_name_in_owner = true
-texture_filter = 1
-clip_contents = true
-anchors_preset = 8
-anchor_left = 0.5
-anchor_top = 0.5
-anchor_right = 0.5
-anchor_bottom = 0.5
-grow_horizontal = 2
-grow_vertical = 2
-texture = ExtResource("3_d58qh")
-expand_mode = 1
-stretch_mode = 1
-
-[node name="Texture" type="TextureRect" parent="Display/ViewportContainer/Viewport/Checkerboard"]
-unique_name_in_owner = true
-clip_contents = true
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-expand_mode = 1
-
-[connection signal="pressed" from="Display/PanelContainer/MarginContainer/HBoxContainer/HBoxContainer/ZoomOut" to="Display" method="_on_zoom_out_pressed"]
-[connection signal="pressed" from="Display/PanelContainer/MarginContainer/HBoxContainer/HBoxContainer/ZoomReset" to="Display" method="_on_zoom_reset_pressed"]
-[connection signal="pressed" from="Display/PanelContainer/MarginContainer/HBoxContainer/HBoxContainer/ZoomIn" to="Display" method="_on_zoom_in_pressed"]
diff --git a/src/interface_elements/number_field.gd b/src/interface_elements/number_field.gd
deleted file mode 100644
index 030c2ce71..000000000
--- a/src/interface_elements/number_field.gd
+++ /dev/null
@@ -1,95 +0,0 @@
-extends HBoxContainer
-
-@onready var up: Button = %Up
-@onready var up_buildup_timer: Timer = %Up/Timer
-@onready var up_repeat_timer: Timer = %Up/Timer2
-@onready var down: Button = %Down
-@onready var down_buildup_timer: Timer = %Down/Timer
-@onready var down_repeat_timer: Timer = %Down/Timer2
-@onready var num_edit: LineEdit = $LineEdit
-
-var min_value := 0
-var max_value := 1024
-var step := 1
-var is_float := true
-var attribute: SVGAttribute
-
-signal value_changed(new_value: float)
-var value: float:
- set(new_value):
- var old_value := value
- value = validate(new_value)
- if value != old_value:
- value_changed.emit(new_value)
-
-
-func _ready() -> void:
- value_changed.connect(_on_value_changed)
- if attribute != null:
- value = attribute.value
- num_edit.text = str(value)
- down.disabled = (value <= min_value)
- up.disabled = (value >= max_value)
-
-func validate(new_value: float) -> float:
- return clampf(new_value, min_value, max_value)
-
-func _on_value_changed(new_value: float) -> void:
- num_edit.text = str(new_value)
- down.disabled = (new_value <= min_value)
- up.disabled = (new_value >= max_value)
- if attribute != null:
- attribute.value = new_value
- SVG.update()
-
-
-func _on_up_button_down() -> void:
- value += step
- up_buildup_timer.start(0.5)
-
-func _on_up_button_up() -> void:
- up_buildup_timer.stop()
- up_repeat_timer.stop()
-
-func _on_up_buildup_timer_timeout() -> void:
- up_repeat_timer.start(0.05)
-
-func _on_up_repeat_timer_timeout() -> void:
- value += step
- if value == max_value:
- up_repeat_timer.stop()
-
-func _on_down_button_down() -> void:
- value -= step
- down_buildup_timer.start(0.5)
-
-func _on_down_button_up() -> void:
- down_buildup_timer.stop()
- down_repeat_timer.stop()
-
-func _on_down_buildup_timer_timeout() -> void:
- down_repeat_timer.start(0.05)
-
-func _on_down_repeat_timer_timeout() -> void:
- value -= step
- if value == min_value:
- down_repeat_timer.stop()
-
-
-# Hacks to make LineEdit bearable.
-
-func _on_focus_entered() -> void:
- get_tree().paused = true
-
-func _on_focus_exited() -> void:
- value = num_edit.text.to_float()
- get_tree().paused = false
-
-func _on_text_submitted(new_text: String) -> void:
- value = new_text.to_float()
- num_edit.release_focus()
-
-func _input(event: InputEvent) -> void:
- if (num_edit.has_focus() and event is InputEventMouseButton and\
- not num_edit.get_global_rect().has_point(event.position)):
- num_edit.release_focus()
diff --git a/src/interface_elements/number_field.tscn b/src/interface_elements/number_field.tscn
deleted file mode 100644
index 52d2f0586..000000000
--- a/src/interface_elements/number_field.tscn
+++ /dev/null
@@ -1,194 +0,0 @@
-[gd_scene load_steps=12 format=3 uid="uid://bp2vpf7g8w8aj"]
-
-[ext_resource type="Script" path="res://src/interface_elements/number_field.gd" id="1_me4eq"]
-[ext_resource type="Texture2D" uid="uid://wburo8uyoqy4" path="res://visual/icons/Up.svg" id="2_qoxs6"]
-[ext_resource type="Texture2D" uid="uid://coda6chhcatal" path="res://visual/icons/Down.svg" id="3_lure8"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yni7n"]
-content_margin_left = 1.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 0.0
-bg_color = Color(0.0075, 0.0075, 0.15, 1)
-border_width_left = 1
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 1
-border_color = Color(0.057, 0.11775, 0.3, 1)
-corner_radius_top_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0ilo3"]
-content_margin_left = 1.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 0.0
-bg_color = Color(0.012, 0.012, 0.2, 1)
-border_width_left = 1
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 1
-border_color = Color(0.076, 0.157, 0.4, 1)
-corner_radius_top_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wue2p"]
-content_margin_left = 1.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 0.0
-bg_color = Color(0.27, 0.4275, 0.9, 1)
-border_width_left = 1
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 1
-border_color = Color(0.6, 0.8, 1, 1)
-corner_radius_top_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_o12w0"]
-content_margin_left = 1.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 0.0
-bg_color = Color(0.06, 0.06, 0.08, 1)
-border_width_left = 1
-border_width_top = 2
-border_width_right = 2
-border_width_bottom = 1
-border_color = Color(0.123, 0.123, 0.15, 1)
-corner_radius_top_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p5khm"]
-content_margin_left = 1.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 2.0
-bg_color = Color(0.00784314, 0.00784314, 0.14902, 1)
-border_width_left = 1
-border_width_top = 1
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.057, 0.11775, 0.3, 1)
-corner_radius_bottom_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0r17a"]
-content_margin_left = 1.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 2.0
-bg_color = Color(0.012, 0.012, 0.2, 1)
-border_width_left = 1
-border_width_top = 1
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.076, 0.157, 0.4, 1)
-corner_radius_bottom_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_flwce"]
-content_margin_left = 1.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 2.0
-bg_color = Color(0.27, 0.4275, 0.9, 1)
-border_width_left = 1
-border_width_top = 1
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.6, 0.8, 1, 1)
-corner_radius_bottom_right = 6
-corner_detail = 16
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_iburr"]
-content_margin_left = 1.0
-content_margin_top = 2.0
-content_margin_right = 2.0
-content_margin_bottom = 2.0
-bg_color = Color(0.06, 0.06, 0.08, 1)
-border_width_left = 1
-border_width_top = 1
-border_width_right = 2
-border_width_bottom = 2
-border_color = Color(0.123, 0.123, 0.15, 1)
-corner_radius_bottom_right = 6
-corner_detail = 16
-
-[node name="NumberField" type="HBoxContainer"]
-custom_minimum_size = Vector2(0, 22)
-offset_right = 52.0
-offset_bottom = 22.0
-theme_override_constants/separation = 0
-script = ExtResource("1_me4eq")
-
-[node name="LineEdit" type="LineEdit" parent="."]
-layout_mode = 2
-size_flags_horizontal = 3
-max_length = 6
-context_menu_enabled = false
-select_all_on_focus = true
-caret_blink = true
-
-[node name="VBoxContainer" type="VBoxContainer" parent="."]
-layout_mode = 2
-theme_override_constants/separation = 0
-
-[node name="Up" type="Button" parent="VBoxContainer"]
-unique_name_in_owner = true
-custom_minimum_size = Vector2(16, 0)
-layout_mode = 2
-size_flags_horizontal = 8
-size_flags_vertical = 3
-focus_mode = 0
-mouse_default_cursor_shape = 2
-theme_override_styles/normal = SubResource("StyleBoxFlat_yni7n")
-theme_override_styles/hover = SubResource("StyleBoxFlat_0ilo3")
-theme_override_styles/pressed = SubResource("StyleBoxFlat_wue2p")
-theme_override_styles/disabled = SubResource("StyleBoxFlat_o12w0")
-action_mode = 0
-icon = ExtResource("2_qoxs6")
-icon_alignment = 1
-expand_icon = true
-
-[node name="Timer" type="Timer" parent="VBoxContainer/Up"]
-wait_time = 0.8
-one_shot = true
-
-[node name="Timer2" type="Timer" parent="VBoxContainer/Up"]
-
-[node name="Down" type="Button" parent="VBoxContainer"]
-unique_name_in_owner = true
-custom_minimum_size = Vector2(16, 0)
-layout_mode = 2
-size_flags_horizontal = 8
-size_flags_vertical = 3
-focus_mode = 0
-mouse_default_cursor_shape = 2
-theme_override_styles/normal = SubResource("StyleBoxFlat_p5khm")
-theme_override_styles/hover = SubResource("StyleBoxFlat_0r17a")
-theme_override_styles/pressed = SubResource("StyleBoxFlat_flwce")
-theme_override_styles/disabled = SubResource("StyleBoxFlat_iburr")
-action_mode = 0
-icon = ExtResource("3_lure8")
-icon_alignment = 1
-expand_icon = true
-
-[node name="Timer" type="Timer" parent="VBoxContainer/Down"]
-wait_time = 0.8
-one_shot = true
-
-[node name="Timer2" type="Timer" parent="VBoxContainer/Down"]
-
-[connection signal="focus_entered" from="LineEdit" to="." method="_on_focus_entered"]
-[connection signal="focus_exited" from="LineEdit" to="." method="_on_focus_exited"]
-[connection signal="text_submitted" from="LineEdit" to="." method="_on_text_submitted"]
-[connection signal="button_down" from="VBoxContainer/Up" to="." method="_on_up_button_down"]
-[connection signal="button_up" from="VBoxContainer/Up" to="." method="_on_up_button_up"]
-[connection signal="timeout" from="VBoxContainer/Up/Timer" to="." method="_on_up_buildup_timer_timeout"]
-[connection signal="timeout" from="VBoxContainer/Up/Timer2" to="." method="_on_up_repeat_timer_timeout"]
-[connection signal="button_down" from="VBoxContainer/Down" to="." method="_on_down_button_down"]
-[connection signal="button_up" from="VBoxContainer/Down" to="." method="_on_down_button_up"]
-[connection signal="timeout" from="VBoxContainer/Down/Timer" to="." method="_on_down_buildup_timer_timeout"]
-[connection signal="timeout" from="VBoxContainer/Down/Timer2" to="." method="_on_down_repeat_timer_timeout"]
diff --git a/src/interface_elements/tag_editor.gd b/src/interface_elements/tag_editor.gd
deleted file mode 100644
index 6fa6093ba..000000000
--- a/src/interface_elements/tag_editor.gd
+++ /dev/null
@@ -1,38 +0,0 @@
-extends HBoxContainer
-
-const NumberField = preload("number_field.tscn")
-const ColorField = preload("color_field.tscn")
-
-@onready var attribute_container: FlowContainer = $AttributeContainer
-@onready var label: Label = $Label
-
-var tag_index: int
-var tag: SVGTag
-
-func _ready() -> void:
- label.text = tag.title
- for attribute_key in tag.attributes:
- var attribute_value: SVGAttribute = tag.attributes[attribute_key]
- var input_field: Control
- match attribute_value.type:
- SVGAttribute.Type.INT:
- input_field = NumberField.instantiate()
- SVGAttribute.Type.FLOAT:
- input_field = NumberField.instantiate()
- input_field.is_float = true
- input_field.min_value = -1024
- SVGAttribute.Type.UFLOAT:
- input_field = NumberField.instantiate()
- input_field.is_float = true
- SVGAttribute.Type.NFLOAT:
- input_field = NumberField.instantiate()
- input_field.is_float = true
- input_field.max_value = 1
- input_field.step = 0.01
- SVGAttribute.Type.COLOR:
- input_field = ColorField.instantiate()
- input_field.attribute = attribute_value
- input_field.tooltip_text = attribute_key
- attribute_container.add_child(input_field)
- SVG.data.tags.insert(tag_index, tag)
- SVG.update()
diff --git a/src/interface_elements/tag_editor.tscn b/src/interface_elements/tag_editor.tscn
deleted file mode 100644
index c5fbbe658..000000000
--- a/src/interface_elements/tag_editor.tscn
+++ /dev/null
@@ -1,29 +0,0 @@
-[gd_scene load_steps=4 format=3 uid="uid://cksx526iftj5d"]
-
-[ext_resource type="Script" path="res://src/interface_elements/tag_editor.gd" id="1_2kong"]
-[ext_resource type="FontFile" uid="uid://c7ury252fql35" path="res://visual/CodeFont.ttf" id="2_51lj2"]
-[ext_resource type="Texture2D" uid="uid://bywp6c8s6meq1" path="res://visual/icons/Cross.svg" id="3_fr0pw"]
-
-[node name="TagEditor" type="HBoxContainer"]
-anchors_preset = 10
-anchor_right = 1.0
-offset_bottom = 20.0
-grow_horizontal = 2
-size_flags_horizontal = 3
-script = ExtResource("1_2kong")
-
-[node name="Label" type="Label" parent="."]
-layout_mode = 2
-theme_override_fonts/font = ExtResource("2_51lj2")
-theme_override_font_sizes/font_size = 14
-
-[node name="AttributeContainer" type="HFlowContainer" parent="."]
-layout_mode = 2
-size_flags_horizontal = 3
-
-[node name="Button" type="Button" parent="."]
-layout_mode = 2
-size_flags_horizontal = 8
-size_flags_vertical = 4
-mouse_default_cursor_shape = 2
-icon = ExtResource("3_fr0pw")
diff --git a/src/interface_elements/view_box_edit.gd b/src/interface_elements/view_box_edit.gd
deleted file mode 100644
index a06111a1b..000000000
--- a/src/interface_elements/view_box_edit.gd
+++ /dev/null
@@ -1,16 +0,0 @@
-extends HBoxContainer
-
-signal view_box_changed(w: int, h: int)
-
-@onready var width_edit: Control = $WidthEdit
-@onready var height_edit: Control = $HeightEdit
-
-func emit_view_box_changed(_new_value := -1.0) -> void:
- view_box_changed.emit(width_edit.value, height_edit.value)
-
-func _ready() -> void:
- height_edit.value = 16
- height_edit.value_changed.connect(emit_view_box_changed)
- width_edit.value = 16
- width_edit.value_changed.connect(emit_view_box_changed)
- emit_view_box_changed()
diff --git a/src/parsers/ColorParser.gd b/src/parsers/ColorParser.gd
new file mode 100644
index 000000000..bed3d2adb
--- /dev/null
+++ b/src/parsers/ColorParser.gd
@@ -0,0 +1,130 @@
+class_name ColorParser extends RefCounted
+
+# The passed text should already be a valid color string.
+static func format_text(text: String) -> String:
+ text = text.strip_edges()
+
+ if ColorParser.is_valid_url(https://github.com/text):
+ return "url("https://github.com/+%20text.substr(4,%20text.length() - 5).strip_edges() + ")"
+
+ if GlobalSettings.color_convert_rgb_to_hex and ColorParser.is_valid_rgb(text):
+ var inside_brackets := text.substr(4, text.length() - 5)
+ var args := inside_brackets.split(",", false)
+ text = "#" +\
+ Color8(args[0].to_int(), args[1].to_int(), args[2].to_int()).to_html(false)
+
+ if GlobalSettings.color_convert_named_to_hex and\
+ ColorParser.is_valid_named(text) and not text in AttributeColor.special_colors:
+ text = AttributeColor.named_colors[text]
+
+ if GlobalSettings.color_use_shorthand_hex_code and text.length() == 7 and\
+ text[0] == "#" and text[1] == text[2] and text[3] == text[4] and text[5] == text[6]:
+ text = "#" + text[1] + text[3] + text[5]
+
+ if GlobalSettings.color_use_short_named_colors:
+ var big_hex := text
+ if ColorParser.is_valid_hex(big_hex) and big_hex.length() == 4:
+ big_hex = "#" + text[1] + text[1] + text[2] + text[2] + text[3] + text[3]
+ if big_hex in AttributeColor.named_colors.values():
+ var text_key: String = AttributeColor.named_colors.find_key(big_hex)
+ if text_key.length() < text.length():
+ text = text_key
+
+ return text
+
+
+static func add_hash_if_hex(color: String) -> String:
+ color = color.strip_edges()
+ if color.is_valid_html_color() and not color.begins_with("#"):
+ color = "#" + color
+ return color
+
+static func is_valid(color: String) -> bool:
+ return is_valid_hex(color) or is_valid_rgb(color) or is_valid_named(color) or\
+ is_valid_url(https://github.com/color)
+
+static func is_valid_hex(color: String) -> bool:
+ color = color.strip_edges()
+ return color.begins_with("#") and color.is_valid_html_color() and\
+ (color.length() == 4 or color.length() == 7)
+
+static func is_valid_rgb(color: String) -> bool:
+ color = color.strip_edges()
+ if not color.begins_with("rgb(") or not color.ends_with(")"):
+ return false
+
+ var channels_str := color.substr(4, color.length() - 5)
+ var channels := channels_str.split(",")
+ if channels.size() == 3 or channels.size() == 4:
+ for channel in channels:
+ if not channel.strip_edges().is_valid_float():
+ return false
+ return true
+ return false
+
+static func is_valid_named(color: String) -> bool:
+ color = color.strip_edges()
+ return color in AttributeColor.special_colors or AttributeColor.named_colors.has(color)
+
+static func is_valid_url(/service/color: String) -> bool:
+ color = color.strip_edges()
+ if not color.begins_with("url(") or not color.ends_with(")"):
+ return false
+ var id := color.substr(4, color.length() - 5).strip_edges().trim_prefix("#")
+ return IDParser.get_id_validity(id) != IDParser.ValidityLevel.INVALID
+
+# URL doesn't have a color interpretation, so it'll give the backup.
+static func string_to_color(color: String, backup := Color.BLACK) -> Color:
+ color = color.strip_edges()
+ if is_valid_named(color):
+ if color == "none":
+ return Color(0, 0, 0, 0)
+ else:
+ return Color(AttributeColor.named_colors[color])
+ elif is_valid_rgb(color):
+ var inside_brackets := color.substr(4, color.length() - 5)
+ var args := inside_brackets.split(",", false)
+ if args.size() == 3:
+ return Color8(args[0].to_int(), args[1].to_int(), args[2].to_int())
+ else:
+ return backup
+ elif is_valid_hex(color):
+ return Color.from_string(color, Color())
+ else:
+ return backup
+
+static func are_colors_same(col1: String, col2: String) -> bool:
+ col1 = col1.strip_edges()
+ col2 = col2.strip_edges()
+ # Ensure that the two colors aren't the same,
+ # but of a type that can't be represented as hex.
+ if col1 == col2:
+ return true
+ elif col1 in AttributeColor.special_colors or col2 in AttributeColor.special_colors:
+ return false
+ elif is_valid_url(https://github.com/col1) != is_valid_url(https://github.com/col2):
+ return false
+ elif is_valid_url(https://github.com/col1) and is_valid_url(https://github.com/col2) and\
+ col1.substr(4, col1.length() - 5).strip_edges() ==\
+ col2.substr(4, col2.length() - 5).strip_edges():
+ return true
+
+ # Represent both colors as 6-digit hex codes to serve as basis for comparison.
+ for i in 2:
+ var col: String = [col1, col2][i]
+ # Start of conversion logic.
+ if is_valid_rgb(col):
+ var inside_brackets := col.substr(4, col.length() - 5)
+ var args := inside_brackets.split(",", false)
+ col = Color8(args[0].to_int(), args[1].to_int(), args[2].to_int()).to_html(false)
+ elif is_valid_hex(col) and col.length() == 4:
+ col = col[1] + col[1] + col[2] + col[2] + col[3] + col[3]
+ elif is_valid_named(col):
+ col = AttributeColor.named_colors[col]
+ col = col.trim_prefix("#")
+ # End of conversion logic.
+ if i == 0:
+ col1 = col
+ elif i == 1:
+ col2 = col
+ return col1 == col2
diff --git a/src/parsers/IDParser.gd b/src/parsers/IDParser.gd
new file mode 100644
index 000000000..1fc9143d1
--- /dev/null
+++ b/src/parsers/IDParser.gd
@@ -0,0 +1,30 @@
+class_name IDParser extends RefCounted
+
+# Invalid XML nametokens are discouraged but still valid.
+# TODO Have logic for displaying them as some kind of warning, say with yellow text.
+enum ValidityLevel {VALID, INVALID_XML_NAMETOKEN, INVALID}
+
+
+static func get_id_validity(id: String) -> ValidityLevel:
+ if id.is_empty() or id[0] == "#":
+ return ValidityLevel.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
diff --git a/src/parsers/ListParser.gd b/src/parsers/ListParser.gd
new file mode 100644
index 000000000..f50af01d5
--- /dev/null
+++ b/src/parsers/ListParser.gd
@@ -0,0 +1,43 @@
+## A parser for [AttributeList].
+class_name ListParser extends RefCounted
+
+static func string_to_list(string: String) -> PackedFloat32Array:
+ var nums_parsed := PackedFloat32Array()
+ var current_num_string: String = ""
+ var comma_exhausted := false
+ var pos := 0
+ while pos < string.length():
+ @warning_ignore("shadowed_global_identifier")
+ var char := string[pos]
+ match char:
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "+", ".":
+ current_num_string += char
+ " ":
+ if current_num_string.is_empty():
+ pos += 1
+ continue
+ else:
+ nums_parsed.append(current_num_string.to_float())
+ current_num_string = ""
+ ",":
+ if comma_exhausted:
+ return nums_parsed
+ elif current_num_string.is_empty():
+ comma_exhausted = true
+ pos += 1
+ continue
+ else:
+ nums_parsed.append(current_num_string.to_float())
+ current_num_string = ""
+ pos += 1
+ if not current_num_string.is_empty():
+ nums_parsed.append(current_num_string.to_float())
+
+ return nums_parsed
+
+static func list_to_string(list: PackedFloat32Array) -> String:
+ var params := PackedStringArray()
+ for element in list:
+ # It's fine to use this parser, AttributeList is just a list of numbers.
+ params.append(NumberParser.num_to_text(element))
+ return " ".join(params)
diff --git a/src/parsers/NumberArrayParser.gd b/src/parsers/NumberArrayParser.gd
new file mode 100644
index 000000000..b494d1636
--- /dev/null
+++ b/src/parsers/NumberArrayParser.gd
@@ -0,0 +1,33 @@
+# A parser for compressed number arrays, used in TransformListParser and PathDataParser.
+class_name NumberArrayParser extends RefCounted
+
+var compress_numbers: bool
+var minimize_spacing: bool
+
+static func basic_num_to_text(num: float, precision := 4) -> String:
+ var text := String.num(num, precision)
+ if text == "-0":
+ text = "0"
+ return text
+
+func num_to_text(num: float, precision := 4) -> String:
+ var text := String.num(num, precision)
+ if compress_numbers:
+ if text.begins_with("0."):
+ text = text.right(-1)
+ elif text.begins_with("-0."):
+ text = text.erase(1)
+ if text == "-0":
+ text = "0"
+ return text
+
+func numstr_arr_to_text(numstr_arr: Array[String]) -> String:
+ var output := ""
+ for i in numstr_arr.size() - 1:
+ var current_numstr := numstr_arr[i]
+ var next_char := numstr_arr[i + 1][0]
+ output += current_numstr
+ if not minimize_spacing or not (("." in current_numstr and next_char == ".") or\
+ next_char in "-+"):
+ output += " "
+ return output + numstr_arr.back()
diff --git a/src/parsers/NumberParser.gd b/src/parsers/NumberParser.gd
new file mode 100644
index 000000000..4ae671a41
--- /dev/null
+++ b/src/parsers/NumberParser.gd
@@ -0,0 +1,45 @@
+class_name NumberParser extends RefCounted
+
+static func num_to_text(number: float) -> String:
+ var output := String.num(number, 4)
+ if GlobalSettings.number_remove_leading_zero and "." in output:
+ if output.begins_with("0"):
+ output = output.right(-1)
+ elif output.begins_with("-0") or output.begins_with("+0"):
+ output = output.erase(1)
+ return output
+
+static func text_to_num(text: String) -> float:
+ return NAN if text.is_empty() else text.to_float()
+
+# The passed text should already be a valid number.
+static func format_text(text: String) -> String:
+ if text.is_empty():
+ return "" # Equivalent to NAN in the app's logic.
+
+ var leading_decimal_point := text.begins_with(".") or text.begins_with("-.") or\
+ text.begins_with("+.")
+ var padded_zeros := 0
+ if "." in text and not GlobalSettings.number_remove_zero_padding:
+ while text.ends_with("0"):
+ text = text.left(-1)
+ padded_zeros += 1
+
+ text = String.num(text.to_float(), 4)
+ if text == "-0":
+ text = "0"
+
+ if leading_decimal_point or\
+ (GlobalSettings.number_remove_leading_zero and "." in text):
+ if text.begins_with("0"):
+ text = text.right(-1)
+ if text.begins_with("-0") or text.begins_with("+0"):
+ text = text.erase(1)
+
+ if padded_zeros > 0:
+ if not "." in text:
+ text += "."
+ text += "0".repeat(padded_zeros)
+ text = text.left(text.find(".") + 5)
+
+ return text
diff --git a/src/parsers/PathDataParser.gd b/src/parsers/PathDataParser.gd
new file mode 100644
index 000000000..361e7f23f
--- /dev/null
+++ b/src/parsers/PathDataParser.gd
@@ -0,0 +1,286 @@
+class_name PathDataParser extends RefCounted
+
+const translation_dict = PathCommand.translation_dict
+
+static func parse_path_data(text: String) -> Array[PathCommand]:
+ return path_commands_from_parsed_data(path_data_to_arrays(text))
+
+static func path_data_to_arrays(text: String) -> Array[Array]:
+ var new_commands: Array[Array] = []
+ var curr_command := ""
+ var prev_command := ""
+ var curr_command_args: Array = []
+ var args_left := 0
+ var comma_exhausted := false # Can ignore many whitespaces, but only one comma.
+
+ var idx := -1
+ while idx < text.length() - 1:
+ idx += 1
+ @warning_ignore("shadowed_global_identifier")
+ var char := text[idx]
+ # Stop parsing if we've hit a character that's not allowed.
+ if not char in "MmLlHhVvAaQqTtCcSsZz0123456789-+e., \n\t\r":
+ return new_commands
+ # Logic for finding out what the next command is going to be.
+ if args_left == 0:
+ match char:
+ "M", "m", "L", "l", "H", "h", "V", "v", "A", "a", "Q", "q", "T", "t",\
+ "C", "c", "S", "s", "Z", "z":
+ curr_command = char
+ args_left = translation_dict[curr_command.to_upper()].new().arg_count
+ " ", "\t", "\n", "\r": continue
+ "-", "+", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
+ if prev_command.is_empty():
+ continue
+
+ match prev_command:
+ "Z", "z":
+ return new_commands
+ "M", "m":
+ curr_command = "L" if prev_command == "M" else "l"
+ args_left = translation_dict[curr_command.to_upper()].new().arg_count
+ "L", "l", "H", "h", "V", "v", "A", "a", "Q", "q", "T", "t", "C", "c",\
+ "S", "s":
+ curr_command = prev_command
+ args_left = translation_dict[curr_command.to_upper()].new().arg_count
+ idx -= 1
+ _: return new_commands
+ # Logic for parsing new numbers until args_left == 0.
+ else:
+ if comma_exhausted and not char in " \n\t\r":
+ comma_exhausted = false
+ # Arc flags are represented by a single character.
+ if curr_command in "Aa" and (args_left == 4 or args_left == 3):
+ match char:
+ "0": curr_command_args.append(0)
+ "1": curr_command_args.append(1)
+ " ", "\n", "\t", "\r": continue
+ ",":
+ if comma_exhausted:
+ return new_commands
+ else:
+ comma_exhausted = true
+ continue
+ _: return new_commands
+ if args_left == 3 and curr_command_args.size() == 5:
+ # The number parsing part doesn't account for whitespace at the start,
+ # so jump over the whitespace here.
+ while idx < text.length() - 1:
+ idx += 1
+ match text[idx]:
+ " ", "\n", "\t", "\r": continue
+ ",":
+ if comma_exhausted:
+ return new_commands
+ else:
+ comma_exhausted = true
+ continue
+ _:
+ idx -= 1
+ break
+ else:
+ # Parse the number.
+ 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 < text.length():
+ char = text[idx]
+ match char:
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
+ idx += 1
+ end_idx += 1
+ if exponent_just_passed:
+ exponent_just_passed = false
+ "-", "+":
+ if end_idx == start_idx or exponent_just_passed:
+ end_idx += 1
+ idx += 1
+ if exponent_just_passed:
+ exponent_just_passed = false
+ else:
+ number_proceed = false
+ idx -= 1
+ ".":
+ if not passed_decimal_point:
+ passed_decimal_point = true
+ end_idx += 1
+ idx += 1
+ else:
+ idx -= 1
+ number_proceed = false
+ " ", "\n", "\t", "\r":
+ if end_idx == start_idx:
+ idx += 1
+ start_idx += 1
+ end_idx += 1
+ continue
+ number_proceed = false
+ ",":
+ if comma_exhausted:
+ return new_commands
+ else:
+ comma_exhausted = true
+ number_proceed = false
+ "e":
+ if passed_decimal_point:
+ return new_commands
+ else:
+ end_idx += 1
+ idx += 1
+ exponent_just_passed = true
+ _:
+ if args_left >= 1 and\
+ not text.substr(start_idx, end_idx - start_idx).is_valid_float():
+ return new_commands
+ else:
+ idx -= 1
+ break
+ curr_command_args.append(
+ text.substr(start_idx, end_idx - start_idx).to_float())
+ args_left -= 1
+
+ # Wrap up the array.
+ if args_left == 0:
+ prev_command = curr_command
+ var finalized_arr: Array = [curr_command]
+ curr_command = ""
+ finalized_arr.append_array(curr_command_args)
+ curr_command_args.clear()
+ new_commands.append(finalized_arr)
+ return new_commands
+
+static func path_commands_from_parsed_data(data: Array[Array]) -> Array[PathCommand]:
+ var cmds: Array[PathCommand] = []
+ for a in data:
+ var new_cmd: PathCommand
+ # The idx 0 element is the command char, the rest are the arguments.
+ var cmd_type = translation_dict[a[0].to_upper()]
+ var relative := Utils.is_string_lower(a[0])
+ match a.size():
+ 1: new_cmd = cmd_type.new(relative)
+ 2: new_cmd = cmd_type.new(a[1], relative)
+ 3: new_cmd = cmd_type.new(a[1], a[2], relative)
+ 5: new_cmd = cmd_type.new(a[1], a[2], a[3], a[4], relative)
+ 7: new_cmd = cmd_type.new(a[1], a[2], a[3], a[4], a[5], a[6], relative)
+ 8: new_cmd = cmd_type.new(a[1], a[2], a[3], a[4], a[5], a[6], a[7], relative)
+ cmds.append(new_cmd)
+ return cmds
+
+
+static func path_commands_to_text(commands_arr: Array[PathCommand]) -> String:
+ var output := ""
+ var num_parser := NumberArrayParser.new()
+ num_parser.compress_numbers = GlobalSettings.path_compress_numbers
+ num_parser.minimize_spacing = GlobalSettings.path_minimize_spacing
+
+ var last_command := ""
+ for i in commands_arr.size():
+ var cmd := commands_arr[i]
+ var cmd_char_capitalized := cmd.command_char.to_upper()
+ if not (GlobalSettings.path_remove_consecutive_commands and\
+ ((cmd_char_capitalized != "M" and last_command == cmd.command_char) or\
+ (last_command == "m" and cmd.command_char == "l") or\
+ (last_command == "M" and cmd.command_char == "L"))):
+ output += cmd.command_char
+ if not GlobalSettings.path_minimize_spacing:
+ output += " "
+ elif i > 0 and GlobalSettings.path_minimize_spacing:
+ var current_char := ""
+ var prev_numstr := ""
+ match cmd_char_capitalized:
+ "A":
+ current_char = num_parser.num_to_text(cmd.rx)[0]
+ prev_numstr = num_parser.num_to_text(commands_arr[i - 1].y)
+ "C", "Q":
+ current_char = num_parser.num_to_text(cmd.x1)[0]
+ prev_numstr = num_parser.num_to_text(commands_arr[i - 1].y)
+ "S":
+ current_char = num_parser.num_to_text(cmd.x2)[0]
+ prev_numstr = num_parser.num_to_text(commands_arr[i - 1].y)
+ "L", "M", "T":
+ current_char = num_parser.num_to_text(cmd.x)[0]
+ prev_numstr = num_parser.num_to_text(commands_arr[i - 1].y)
+ "H":
+ current_char = num_parser.num_to_text(cmd.x)[0]
+ prev_numstr = num_parser.num_to_text(commands_arr[i - 1].x)
+ "V":
+ current_char = num_parser.num_to_text(cmd.y)[0]
+ prev_numstr = num_parser.num_to_text(+commands_arr[i - 1].y)
+ if not GlobalSettings.path_minimize_spacing or not\
+ (("." in prev_numstr and current_char == ".") or current_char in "-+"):
+ output += " "
+
+ last_command = cmd.command_char
+ match cmd_char_capitalized:
+ "A":
+ output += num_parser.numstr_arr_to_text([num_parser.num_to_text(cmd.rx),
+ num_parser.num_to_text(cmd.ry), num_parser.num_to_text(cmd.rot, 2)])
+ if GlobalSettings.path_remove_spacing_after_flags:
+ output += (" 0" if cmd.large_arc_flag == 0 else " 1") +\
+ ("0" if cmd.sweep_flag == 0 else "1")
+ else:
+ output += (" 0 " if cmd.large_arc_flag == 0 else " 1 ") +\
+ ("0 " if cmd.sweep_flag == 0 else "1 ")
+ output += num_parser.numstr_arr_to_text([num_parser.num_to_text(cmd.x),
+ num_parser.num_to_text(cmd.y)])
+ "C":
+ output += num_parser.numstr_arr_to_text([num_parser.num_to_text(cmd.x1),
+ num_parser.num_to_text(cmd.y1), num_parser.num_to_text(cmd.x2),
+ num_parser.num_to_text(cmd.y2), num_parser.num_to_text(cmd.x),
+ num_parser.num_to_text(cmd.y)])
+ "Q":
+ output += num_parser.numstr_arr_to_text([num_parser.num_to_text(cmd.x1),
+ num_parser.num_to_text(cmd.y1), num_parser.num_to_text(cmd.x),
+ num_parser.num_to_text(cmd.y)])
+ "S":
+ output += num_parser.numstr_arr_to_text([num_parser.num_to_text(cmd.x2),
+ num_parser.num_to_text(cmd.y2), num_parser.num_to_text(cmd.x),
+ num_parser.num_to_text(cmd.y)])
+ "L", "M", "T":
+ output += num_parser.numstr_arr_to_text([num_parser.num_to_text(cmd.x),
+ num_parser.num_to_text(cmd.y)])
+ "H":
+ output += num_parser.num_to_text(cmd.x)
+ "V":
+ output += num_parser.num_to_text(cmd.y)
+ _: continue
+ if not GlobalSettings.path_minimize_spacing:
+ output += " "
+
+ output = output.rstrip(" ")
+ return output
+
+
+# DEBUG
+
+#static func _static_init() -> void:
+ #var tests := {
+ #"Jerky": [],
+ #"M 3s 6 h 6 v 3 z": [],
+ #"M 3 s6 h 6 v 3 z": [],
+ #"M 3 .s6 h 6 v 3 z": [],
+ #" 0 2": [],
+ #"M 0 0": [["M", 0.0, 0.0]],
+ #"M2 1 L3 4": [["M", 2.0, 1.0], ["L", 3.0, 4.0]],
+ #"m2 0 3 4": [["m", 2.0, 0.0], ["l", 3.0, 4.0]],
+ #"m-2.3.7-4,4": [["m", -2.3, 0.7], ["l", -4.0, 4.0]],
+ #"m2 3a7 3 0 101.2.3": [["m", 2.0, 3.0], ["a", 7.0, 3.0, 0.0, 1, 0, 1.2, 0.3]],
+ #"M 2 0 c3 2-.6.8 11.0 3Jh3": [["M", 2.0, 0.0], ["c", 3.0, 2.0, -0.6, 0.8, 11.0, 3.0]],
+ #"z": [["z"]],
+ #"M 0 0 z 2 3": [["M", 0.0, 0.0], ["z"]],
+ #"M3e1 4e-2": [["M", 3e1, 4e-2]],
+ #"M5,1 A4,4,0,1,1,5,9": [["M", 5.0, 1.0], ["A", 4.0, 4.0, 0.0, 1, 1, 5.0, 9.0]],
+ #}
+ #
+ #var tests_passed := true
+ #for test in tests.keys():
+ #var result := PathDataParser.path_data_to_arrays(test)
+ #var expected: Array = tests[test]
+ #if result != expected:
+ #tests_passed = false
+ #print('"' + test + '" generated ' + str(result) + ', expected ' + str(expected))
+ #else:
+ #print('"' + test + '" generated ' + str(result) + ' (SUCCESS)')
+ #assert(tests_passed)
diff --git a/src/parsers/SVGDB.gd b/src/parsers/SVGDB.gd
new file mode 100644
index 000000000..e2f8594b4
--- /dev/null
+++ b/src/parsers/SVGDB.gd
@@ -0,0 +1,29 @@
+class_name SVGDB extends RefCounted
+
+const known_tags = ["svg", "circle", "ellipse", "rect", "path", "line"]
+
+const known_tag_attributes = { # Dictionary{String: Array[String]}
+ "svg": TagSVG.known_attributes,
+ "circle": TagCircle.known_shape_attributes + TagCircle.known_inheritable_attributes,
+ "ellipse": TagEllipse.known_shape_attributes + TagEllipse.known_inheritable_attributes,
+ "rect": TagRect.known_shape_attributes + TagRect.known_inheritable_attributes,
+ "path": TagPath.known_shape_attributes + TagPath.known_inheritable_attributes,
+ "line": TagLine.known_shape_attributes + TagLine.known_inheritable_attributes,
+}
+
+static func is_tag_known(tag_name: String) -> bool:
+ return tag_name in known_tags
+
+static func is_attribute_known(tag_name: String, attribute_name: String) -> bool:
+ if not known_tag_attributes.has(tag_name):
+ return false
+ return attribute_name in known_tag_attributes[tag_name]
+
+static func get_tag_icon(tag_name: String) -> Texture2D:
+ match tag_name:
+ "circle": return TagCircle.icon
+ "ellipse": return TagEllipse.icon
+ "rect": return TagRect.icon
+ "path": return TagPath.icon
+ "line": return TagLine.icon
+ _: return TagUnknown.icon
diff --git a/src/parsers/SVGHighlighter.gd b/src/parsers/SVGHighlighter.gd
new file mode 100644
index 000000000..5c7aff84f
--- /dev/null
+++ b/src/parsers/SVGHighlighter.gd
@@ -0,0 +1,118 @@
+## A syntax highlighter for SVGs, allows for more flexibility than CodeHighlighter.
+class_name SVGHighlighter extends SyntaxHighlighter
+
+@export var symbol_color := Color("abc9ff")
+@export var tag_color := Color("ff8ccc")
+@export var attribute_color := Color("bce0ff")
+@export var string_color := Color("a1ffe0")
+@export var comment_color := Color("cdcfd280")
+@export var text_color := Color("cdcfeaac")
+@export var error_color := Color("ff866b")
+
+var unknown_tag_color := tag_color.darkened(0.3)
+var unknown_attribute_color := attribute_color.darkened(0.3)
+
+func is_attribute_symbol(c: String) -> bool:
+ return (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or\
+ (c >= "0" and c <= "9") or c == "-" or c == ":"
+
+func _get_line_syntax_highlighting(line: int) -> Dictionary:
+ var svg_text := get_text_edit().get_line(line)
+ if svg_text.is_empty():
+ return {}
+
+ var color_map := {} # Dictionary{int: Dictionary{String: Color}}
+ var parser := XMLParser.new()
+ parser.open_buffer(svg_text.to_utf8_buffer())
+ while parser.read() == OK:
+ var offset := parser.get_node_offset()
+ match parser.get_node_type():
+ XMLParser.NODE_COMMENT:
+ color_map[offset] = {"color": comment_color}
+ XMLParser.NODE_CDATA, XMLParser.NODE_TEXT:
+ color_map[offset] = {"color": text_color}
+ XMLParser.NODE_ELEMENT_END:
+ offset = svg_text.find("<", offset)
+ var tag_name := parser.get_node_name()
+ color_map[offset] = {"color": symbol_color}
+ offset += 2
+ color_map[offset] = {"color":
+ tag_color if SVGDB.is_tag_known(tag_name) else unknown_tag_color}
+ offset += tag_name.length()
+ color_map[offset] = {"color": symbol_color}
+ XMLParser.NODE_ELEMENT:
+ offset = svg_text.find("<", offset)
+ var tag_name := parser.get_node_name()
+ color_map[offset] = {"color": symbol_color}
+ offset += 1
+ color_map[offset] = {"color":
+ tag_color if SVGDB.is_tag_known(tag_name) else unknown_tag_color}
+ offset += tag_name.length()
+ color_map[offset] = {"color": symbol_color}
+
+ # Parsing stuff inside an element.
+ if offset >= svg_text.length() or svg_text[offset] == ">":
+ continue
+ offset += 1
+ # Find where the current tag ends to be safe.
+ var next_end: int
+ var next_end_a := svg_text.find("/>", offset)
+ var next_end_b := svg_text.find(">", offset)
+ if next_end_a == -1 and next_end_b != -1:
+ next_end = next_end_b
+ elif next_end_b == -1 and next_end_a != -1:
+ next_end = next_end_a
+ elif next_end_b != -1 and next_end_a != -1:
+ next_end = mini(next_end_a, next_end_b)
+ else:
+ return color_map
+
+ # Highlight the attribute name and equal sign.
+ while offset < next_end:
+ var next_equal_sign := svg_text.find("=", offset)
+ if next_equal_sign != -1 and next_equal_sign < next_end:
+ var is_known := SVGDB.is_attribute_known(tag_name,
+ svg_text.substr(offset, next_equal_sign - offset).strip_edges())
+ while not is_attribute_symbol(svg_text[offset]):
+ offset += 1
+ color_map[offset] = {"color": attribute_color if is_known\
+ else unknown_attribute_color}
+ while offset < next_equal_sign:
+ if not is_attribute_symbol(svg_text[offset]):
+ color_map[offset] = {"color": error_color}
+ break
+ offset += 1
+ offset = next_equal_sign
+ color_map[offset] = {"color": symbol_color}
+
+ # Highlight the attribute value.
+ offset += 1
+ color_map[offset] = {"color": error_color}
+ var next_double_quote_pos := svg_text.find('"', offset)
+ var next_single_quote_pos := svg_text.find("'", offset)
+ var in_double_quote := true
+ var next_quote_pos := next_double_quote_pos
+ if next_single_quote_pos != -1 and (next_double_quote_pos == -1 or\
+ next_single_quote_pos < next_double_quote_pos):
+ in_double_quote = false
+ next_quote_pos = next_single_quote_pos
+ offset = next_quote_pos
+ color_map[offset] = {"color": string_color}
+ if next_quote_pos == -1 or next_quote_pos >\
+ mini(svg_text.find("/", offset), svg_text.find(">", offset)):
+ offset = mini(svg_text.find("/", offset), svg_text.find(">", offset))
+ break
+ else:
+ next_quote_pos = svg_text.find(
+ '"' if in_double_quote else "'", offset + 1)
+ offset = next_quote_pos + 1
+ color_map[offset] = {"color": symbol_color}
+ if next_quote_pos == -1:
+ return color_map
+ else:
+ offset = next_quote_pos + 1
+ # Finish parsing.
+ color_map[offset] = {"color": symbol_color}
+
+ return color_map
+
diff --git a/src/parsers/SVGParser.gd b/src/parsers/SVGParser.gd
new file mode 100644
index 000000000..13b44ef2b
--- /dev/null
+++ b/src/parsers/SVGParser.gd
@@ -0,0 +1,175 @@
+class_name SVGParser extends RefCounted
+
+# Tags that don't make sense without other tags inside them.
+const shorthand_tag_exceptions = ["g", "linearGradient, radialGradient"]
+
+static func svg_to_text(svg_tag: TagSVG) -> String:
+ if svg_tag == null:
+ return ""
+
+ var w: String = svg_tag.attributes.width.get_value()
+ var h: String = svg_tag.attributes.height.get_value()
+ var viewbox: String = svg_tag.attributes.viewBox.get_value()
+
+ var text := ''
+ for inner_tag in svg_tag.child_tags:
+ text += _tag_to_text(inner_tag)
+ text += ' '
+
+ if GlobalSettings.xml_add_trailing_newline:
+ text += '\n'
+
+ return text
+
+static func _tag_to_text(tag: Tag) -> String:
+ var text := ""
+ text += '<' + tag.name
+ for attribute_key in tag.attributes:
+ var attribute: Attribute = tag.attributes[attribute_key]
+ var value := attribute.get_value()
+ if value == attribute.default:
+ continue
+
+ text += " " + attribute_key + '="' + value + '"'
+
+ for attribute in tag.unknown_attributes:
+ text += " " + attribute.name + '="' + attribute.get_value() + '"'
+
+ if tag.is_standalone() and GlobalSettings.xml_shorthand_tags and\
+ not tag.name in shorthand_tag_exceptions:
+ text += '/>'
+ else:
+ text += '>'
+ for child_tag in tag.child_tags:
+ text += _tag_to_text(child_tag)
+ text += '' + tag.name + '>'
+
+ return text
+
+
+enum ParseError {OK, ERR_NOT_SVG, ERR_IMPROPER_NESTING}
+
+class ParseResult extends RefCounted:
+ var error: SVGParser.ParseError
+ var svg: TagSVG
+
+ func _init(err_id: SVGParser.ParseError, result: TagSVG = null) -> void:
+ error = err_id
+ svg = result
+
+static func get_error_stringname(parse_error: ParseError) -> StringName:
+ match parse_error:
+ ParseError.ERR_NOT_SVG: return &"#err_not_svg"
+ ParseError.ERR_IMPROPER_NESTING: return &"#err_improper_nesting"
+ _: return &""
+
+# Returns a StringName if there's an error.
+static func text_to_svg(text: String) -> ParseResult:
+ if text.is_empty():
+ return ParseResult.new(ParseError.ERR_NOT_SVG)
+
+ var svg_tag := TagSVG.new()
+ var parser := XMLParser.new()
+ parser.open_buffer(text.to_utf8_buffer())
+ var unclosed_tag_stack: Array[Tag] = [svg_tag]
+
+ # Remove everything before the first SVG tag.
+ var describes_svg := false
+
+ while parser.read() == OK:
+ if parser.get_node_type() == XMLParser.NODE_ELEMENT:
+ if parser.get_node_name() == "svg":
+ describes_svg = true
+
+ var attrib_dict := {}
+ for i in range(parser.get_attribute_count()):
+ attrib_dict[parser.get_attribute_name(i)] = parser.get_attribute_value(i)
+ # width, height, and viewBox don't have defaults.
+ if attrib_dict.has("width"):
+ svg_tag.attributes.width.set_value(attrib_dict["width"],
+ Attribute.SyncMode.SILENT)
+ if attrib_dict.has("height"):
+ svg_tag.attributes.height.set_value(attrib_dict["height"],
+ Attribute.SyncMode.SILENT)
+ if attrib_dict.has("viewBox"):
+ svg_tag.attributes.viewBox.set_value(attrib_dict["viewBox"],
+ Attribute.SyncMode.SILENT)
+ svg_tag.update_cache()
+
+ var unknown: Array[AttributeUnknown] = []
+ for element in attrib_dict:
+ if svg_tag.attributes.has(element):
+ var attribute: Attribute = svg_tag.attributes[element]
+ attribute.set_value(attrib_dict[element], Attribute.SyncMode.SILENT)
+ else:
+ unknown.append(AttributeUnknown.new(element, attrib_dict[element]))
+ svg_tag.set_unknown_attributes(unknown)
+
+ var node_offset := parser.get_node_offset()
+ var closure_pos := text.find("/>", node_offset)
+ if closure_pos == -1 or closure_pos >= text.find(">", node_offset):
+ unclosed_tag_stack.append(svg_tag)
+
+ break
+
+ if not describes_svg:
+ return ParseResult.new(ParseError.ERR_NOT_SVG)
+ # Parse everything until the SVG closing tag.
+ while parser.read() == OK:
+ match parser.get_node_type():
+ XMLParser.NODE_ELEMENT:
+ var node_name := parser.get_node_name()
+ var attrib_dict := {}
+ for i in range(parser.get_attribute_count()):
+ attrib_dict[parser.get_attribute_name(i)] = parser.get_attribute_value(i)
+
+ var tag: Tag
+ match node_name:
+ "circle": tag = TagCircle.new()
+ "ellipse": tag = TagEllipse.new()
+ "rect": tag = TagRect.new()
+ "path": tag = TagPath.new()
+ "line": tag = TagLine.new()
+ _: tag = TagUnknown.new(node_name)
+
+ var unknown: Array[AttributeUnknown] = []
+ for element in attrib_dict:
+ if tag.attributes.has(element):
+ var attribute: Attribute = tag.attributes[element]
+ attribute.set_value(attrib_dict[element], Attribute.SyncMode.SILENT)
+ else:
+ unknown.append(AttributeUnknown.new(element, attrib_dict[element]))
+ tag.set_unknown_attributes(unknown)
+
+ # Check if we're entering or exiting the tag.
+ var node_offset := parser.get_node_offset()
+ var closure_pos := text.find("/>", node_offset)
+ if closure_pos == -1 or closure_pos >= text.find(">", node_offset):
+ unclosed_tag_stack.append(tag)
+ else:
+ unclosed_tag_stack.back().child_tags.append(tag)
+ XMLParser.NODE_ELEMENT_END:
+ if unclosed_tag_stack.is_empty():
+ return ParseResult.new(ParseError.ERR_IMPROPER_NESTING)
+ else:
+ var closed_tag: Tag = unclosed_tag_stack.pop_back()
+ if closed_tag.name != parser.get_node_name():
+ return ParseResult.new(ParseError.ERR_IMPROPER_NESTING)
+ if unclosed_tag_stack.size() > 1:
+ unclosed_tag_stack.back().child_tags.append(closed_tag)
+ else:
+ break
+
+ return ParseResult.new(ParseError.OK, svg_tag)
diff --git a/src/parsers/TransformListParser.gd b/src/parsers/TransformListParser.gd
new file mode 100644
index 000000000..188eb46ad
--- /dev/null
+++ b/src/parsers/TransformListParser.gd
@@ -0,0 +1,170 @@
+class_name TransformListParser extends RefCounted
+
+static func transform_list_to_text(
+transform_list: Array[AttributeTransform.Transform]) -> String:
+ var output := ""
+ var num_parser := NumberArrayParser.new()
+ num_parser.compress_numbers = GlobalSettings.transform_compress_numbers
+ num_parser.minimize_spacing = GlobalSettings.transform_minimize_spacing
+
+ for t in transform_list:
+ if t is AttributeTransform.TransformMatrix:
+ output += "matrix(%s) " % num_parser.numstr_arr_to_text([
+ num_parser.num_to_text(t.x1), num_parser.num_to_text(t.x2),
+ num_parser.num_to_text(t.y1), num_parser.num_to_text(t.y2),
+ num_parser.num_to_text(t.o1), num_parser.num_to_text(t.o2)])
+ elif t is AttributeTransform.TransformTranslate:
+ if t.y == 0 and GlobalSettings.transform_remove_unnecessary_params:
+ output += "translate(%s) " % num_parser.num_to_text(t.x)
+ else:
+ output += "translate(%s) " % num_parser.numstr_arr_to_text([
+ num_parser.num_to_text(t.x), num_parser.num_to_text(t.y)])
+ elif t is AttributeTransform.TransformRotate:
+ if t.x == 0 and t.y == 0 and GlobalSettings.transform_remove_unnecessary_params:
+ output += "rotate(%s) " % num_parser.num_to_text(t.deg)
+ else:
+ output += "rotate(%s) " % num_parser.numstr_arr_to_text([
+ num_parser.num_to_text(t.deg),
+ num_parser.num_to_text(t.x), num_parser.num_to_text(t.y)])
+ elif t is AttributeTransform.TransformScale:
+ if t.x == t.y and GlobalSettings.transform_remove_unnecessary_params:
+ output += "scale(%s) " % num_parser.num_to_text(t.x)
+ else:
+ output += "scale(%s) " % num_parser.numstr_arr_to_text([
+ num_parser.num_to_text(t.x), num_parser.num_to_text(t.y)])
+ elif t is AttributeTransform.TransformSkewX:
+ output += "skewX(%s) " % num_parser.num_to_text(t.x)
+ elif t is AttributeTransform.TransformSkewY:
+ output += "skewY(%s) " % num_parser.num_to_text(t.y)
+
+ return output.trim_suffix(" ")
+
+static func text_to_transform_list(text: String) -> Array[AttributeTransform.Transform]:
+ if text.is_empty():
+ return []
+
+ var output: Array[AttributeTransform.Transform] = []
+ text = text.strip_edges()
+ var transforms := text.split(")", false)
+ for transform in transforms:
+ var transform_info := transform.split("(")
+ if transform_info.size() != 2:
+ return []
+
+ var transform_params := transform_info[1].strip_edges()
+ var nums: Array[float] = []
+
+ # Parse the numbers.
+ # TODO maybe this can be moved to NumberArrayParser.
+ var comma_exhausted := false # Can ignore many whitespaces, but only one comma.
+ var idx := -1
+ while idx < transform_params.length() - 1:
+ idx += 1
+ @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():
+ char = transform_params[idx]
+ match char:
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
+ idx += 1
+ end_idx += 1
+ if exponent_just_passed:
+ exponent_just_passed = false
+ "-", "+":
+ if end_idx == start_idx or exponent_just_passed:
+ end_idx += 1
+ idx += 1
+ if exponent_just_passed:
+ exponent_just_passed = false
+ else:
+ number_proceed = false
+ idx -= 1
+ ".":
+ if not passed_decimal_point:
+ passed_decimal_point = true
+ end_idx += 1
+ idx += 1
+ else:
+ idx -= 1
+ number_proceed = false
+ " ", "\n", "\t", "\r":
+ if end_idx == start_idx:
+ idx += 1
+ start_idx += 1
+ end_idx += 1
+ continue
+ number_proceed = false
+ ",":
+ if comma_exhausted:
+ return []
+ else:
+ comma_exhausted = true
+ number_proceed = false
+ "e":
+ if passed_decimal_point:
+ return []
+ else:
+ end_idx += 1
+ idx += 1
+ exponent_just_passed = true
+ _:
+ if not transform_params.substr(start_idx,
+ end_idx - start_idx).is_valid_float():
+ return []
+ else:
+ idx -= 1
+ break
+ nums.append(transform_params.substr(start_idx, end_idx - start_idx).to_float())
+
+ match transform_info[0].strip_edges():
+ "matrix":
+ if nums.size() == 6:
+ output.append(AttributeTransform.TransformMatrix.new(nums[0], nums[1],
+ nums[2], nums[3], nums[4], nums[5]))
+ else:
+ return []
+ "translate":
+ if nums.size() == 1:
+ output.append(AttributeTransform.TransformTranslate.new(nums[0], 0.0))
+ elif nums.size() == 2:
+ output.append(AttributeTransform.TransformTranslate.new(nums[0], nums[1]))
+ else:
+ return []
+ "rotate":
+ if nums.size() == 1:
+ output.append(AttributeTransform.TransformRotate.new(nums[0], 0.0, 0.0))
+ elif nums.size() == 3:
+ output.append(AttributeTransform.TransformRotate.new(
+ nums[0], nums[1], nums[2]))
+ else:
+ return []
+ "scale":
+ if nums.size() == 1:
+ output.append(AttributeTransform.TransformScale.new(nums[0], nums[0]))
+ elif nums.size() == 2:
+ output.append(AttributeTransform.TransformScale.new(nums[0], nums[1]))
+ else:
+ return []
+ "skewX":
+ if nums.size() == 1:
+ output.append(AttributeTransform.TransformSkewX.new(nums[0]))
+ else:
+ return []
+ "skewY":
+ if nums.size() == 1:
+ output.append(AttributeTransform.TransformSkewY.new(nums[0]))
+ else:
+ return []
+ _:
+ return []
+
+ return output
diff --git a/src/shaders/color_wheel.gdshader b/src/shaders/color_wheel.gdshader
new file mode 100644
index 000000000..8dfccc5f0
--- /dev/null
+++ b/src/shaders/color_wheel.gdshader
@@ -0,0 +1,22 @@
+shader_type canvas_item;
+
+uniform float v = 1.0;
+
+void fragment() {
+ float x = UV.x - 0.5;
+ float y = UV.y - 0.5;
+ float a = atan(y, x);
+ x += 0.001;
+ y += 0.001;
+ float b = float(sqrt(x * x + y * y) < 0.5);
+ x -= 0.002;
+ float b2 = float(sqrt(x * x + y * y) < 0.5);
+ y -= 0.002;
+ float b3 = float(sqrt(x * x + y * y) < 0.5);
+ x += 0.002;
+ float b4 = float(sqrt(x * x + y * y) < 0.5);
+
+ COLOR = vec4(mix(vec3(1.0), clamp(abs(fract(vec3((a - TAU) / TAU) +
+ vec3(1.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - vec3(3.0)) - vec3(1.0), 0.0, 1.0),
+ ((float(sqrt(x * x + y * y)) * 2.0)) / 1.0) * vec3(v), (b + b2 + b3 + b4) / 4.0);
+}
\ No newline at end of file
diff --git a/src/shaders/slider_visuals.gdshader b/src/shaders/slider_visuals.gdshader
new file mode 100644
index 000000000..5c698c7f1
--- /dev/null
+++ b/src/shaders/slider_visuals.gdshader
@@ -0,0 +1,45 @@
+shader_type canvas_item;
+
+uniform vec3 base_color = vec3(1.0, 1.0, 1.0);
+// 0 = Red, 1 = Green, 2 = Blue, 3 = Hue, 4 = Saturation, 5 = Value
+uniform int interpolation = 0;
+uniform bool horizontal = true;
+uniform bool inverted = false;
+
+vec3 rgb2hsv(vec3 c) {
+ vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+ vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+ vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+ float d = q.x - min(q.w, q.y);
+ float e = 1.0e-10;
+ return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
+}
+
+vec3 hsv2rgb(vec3 c) {
+ vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+ vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+ return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
+void fragment() {
+ float offset = (horizontal ? UV.x : UV.y);
+ offset = (inverted ? 1.0 - offset : offset);
+
+ if (interpolation == 0) {
+ COLOR = vec4(offset, base_color.g, base_color.b, 1.0);
+ } else if (interpolation == 1) {
+ COLOR = vec4(base_color.r, offset, base_color.b, 1.0);
+ } else if (interpolation == 2) {
+ COLOR = vec4(base_color.r, base_color.g, offset, 1.0);
+ } else {
+ vec3 hsv = rgb2hsv(base_color.rgb);
+ if (interpolation == 3) {
+ hsv.x = offset;
+ } else if (interpolation == 4) {
+ hsv.y = offset;
+ } else if (interpolation == 5) {
+ hsv.z = offset;
+ }
+ COLOR = vec4(hsv2rgb(hsv), 1.0);
+ }
+}
\ No newline at end of file
diff --git a/src/shaders/zoom_shader.gdshader b/src/shaders/zoom_shader.gdshader
new file mode 100644
index 000000000..4c002f085
--- /dev/null
+++ b/src/shaders/zoom_shader.gdshader
@@ -0,0 +1,7 @@
+shader_type canvas_item;
+
+uniform float uv_scale = 1.0;
+
+void fragment() {
+ COLOR = texture(TEXTURE, UV * uv_scale);
+}
\ No newline at end of file
diff --git a/src/ui_elements/BetterLineEdit.gd b/src/ui_elements/BetterLineEdit.gd
new file mode 100644
index 000000000..ca4b893d1
--- /dev/null
+++ b/src/ui_elements/BetterLineEdit.gd
@@ -0,0 +1,106 @@
+## A LineEdit with a few tweaks to make it nicer to use.
+class_name BetterLineEdit extends LineEdit
+
+signal text_change_canceled
+
+const code_font = preload("res://visual/fonts/FontMono.ttf")
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+
+var hovered := false
+
+@export var hover_stylebox: StyleBox ## Overlayed on top when you hover the LineEdit.
+@export var focus_stylebox: StyleBox ## Overlayed on top when the LineEdit is focused.
+@export var code_font_tooltip := false ## Use the mono font for the tooltip.
+
+func _init() -> void:
+ context_menu_enabled = false
+ caret_blink = true
+ caret_blink_interval = 0.6
+
+func _ready() -> void:
+ focus_entered.connect(_on_focus_entered)
+ focus_exited.connect(_on_focus_exited)
+ mouse_exited.connect(_on_mouse_exited)
+ text_submitted.connect(release_focus.unbind(1))
+
+func _input(event: InputEvent) -> void:
+ if has_focus() and event is InputEventMouseButton:
+ if event.is_pressed() and not get_global_rect().has_point(event.position):
+ release_focus()
+ text_submitted.emit(text)
+ elif event.is_released() and first_click and not has_selection():
+ first_click = false
+ select_all()
+
+var tree_was_paused_before := false
+var first_click := false
+var text_before_focus := ""
+
+func _on_focus_entered() -> void:
+ process_mode = PROCESS_MODE_ALWAYS
+ tree_was_paused_before = get_tree().paused
+ first_click = true
+ text_before_focus = text
+ if not tree_was_paused_before:
+ get_tree().paused = true
+
+func _on_focus_exited() -> void:
+ process_mode = PROCESS_MODE_INHERIT
+ first_click = false
+ if not tree_was_paused_before:
+ get_tree().paused = false
+ if Input.is_action_pressed(&"ui_cancel"):
+ text = text_before_focus
+ text_change_canceled.emit()
+
+
+func _on_mouse_exited() -> void:
+ hovered = false
+ queue_redraw()
+
+func _draw() -> void:
+ if editable:
+ if has_focus() and focus_stylebox != null:
+ draw_style_box(focus_stylebox, Rect2(Vector2.ZERO, size))
+ elif hovered and hover_stylebox != null:
+ draw_style_box(hover_stylebox, Rect2(Vector2.ZERO, size))
+
+func _make_custom_tooltip(for_text: String) -> Object:
+ if code_font_tooltip:
+ var label := Label.new()
+ label.begin_bulk_theme_override()
+ label.add_theme_font_override(&"font", code_font)
+ label.add_theme_font_size_override(&"font_size", 13)
+ label.end_bulk_theme_override()
+ label.text = for_text
+ return label
+ else:
+ return null
+
+
+func _gui_input(event: InputEvent) -> void:
+ mouse_filter = Utils.mouse_filter_pass_non_drag_events(event)
+
+ if event is InputEventMouseMotion and event.button_mask == 0:
+ hovered = true
+ queue_redraw()
+ elif event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_RIGHT:
+ grab_focus()
+ var context_popup := ContextPopup.instantiate()
+ var btn_arr: Array[Button] = [
+ Utils.create_btn(tr(&"#undo"), menu_option.bind(LineEdit.MENU_UNDO)),
+ Utils.create_btn(tr(&"#redo"), menu_option.bind(LineEdit.MENU_REDO)),
+ Utils.create_btn(tr(&"#copy"), menu_option.bind(LineEdit.MENU_COPY),
+ text.is_empty()),
+ Utils.create_btn(tr(&"#paste"), menu_option.bind(LineEdit.MENU_PASTE),
+ !DisplayServer.clipboard_has()),
+ Utils.create_btn(tr(&"#cut"), menu_option.bind(LineEdit.MENU_CUT),
+ text.is_empty()),
+ ]
+
+ add_child(context_popup)
+ context_popup.set_button_array(btn_arr, true, 72)
+ var viewport := get_viewport()
+ Utils.popup_under_pos(context_popup, viewport.get_mouse_position(), viewport)
+ accept_event()
diff --git a/src/ui_elements/BetterTabContainer.gd b/src/ui_elements/BetterTabContainer.gd
new file mode 100644
index 000000000..2bfad856f
--- /dev/null
+++ b/src/ui_elements/BetterTabContainer.gd
@@ -0,0 +1,19 @@
+## A TabContainer that automatically localizes tab titles.
+class_name BetterTabContainer extends TabContainer
+
+var tab_keys: Array[StringName]
+
+# Localize tab titles.
+func _ready() -> void:
+ for i in get_tab_count():
+ tab_keys.append(StringName(get_tab_title(i)))
+ translate_titles()
+
+func _notification(what: int) -> void:
+ if what == NOTIFICATION_TRANSLATION_CHANGED:
+ translate_titles()
+
+
+func translate_titles() -> void:
+ for i in tab_keys.size():
+ set_tab_title(i, tr(tab_keys[i]))
diff --git a/src/ui_elements/BetterTextEdit.gd b/src/ui_elements/BetterTextEdit.gd
new file mode 100644
index 000000000..665efe84d
--- /dev/null
+++ b/src/ui_elements/BetterTextEdit.gd
@@ -0,0 +1,127 @@
+## A TextEdit with some improvements.
+class_name BetterTextEdit extends TextEdit
+
+const code_font = preload("res://visual/fonts/FontMono.ttf")
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const caret_color = Color("defd")
+
+var surface := RenderingServer.canvas_item_create()
+var timer := Timer.new()
+
+var hovered := false
+
+@export var hover_stylebox: StyleBox ## Overlayed on top when you hover the TextEdit.
+@export var block_non_ascii: bool ## Blocks non-ASCII characters.
+
+func _init() -> void:
+ context_menu_enabled = false
+ wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
+ scroll_smooth = true
+ scroll_v_scroll_speed = 30.0
+ caret_multiple = false
+ highlight_all_occurrences = true
+
+func _ready() -> void:
+ RenderingServer.canvas_item_set_parent(surface, get_canvas_item())
+ add_child(timer)
+ timer.timeout.connect(blink)
+ get_v_scroll_bar().value_changed.connect(redraw_caret.unbind(1))
+ get_h_scroll_bar().value_changed.connect(redraw_caret.unbind(1))
+ mouse_exited.connect(_on_mouse_exited)
+
+
+# Workaround for there not being a built-in overtype_mode_changed signal.
+var overtype_mode := false
+func _process(_delta: float) -> void:
+ if is_overtype_mode_enabled() != overtype_mode:
+ overtype_mode = not overtype_mode
+ redraw_caret()
+
+func redraw_caret() -> void:
+ await get_tree().process_frame # Buggy with backspace otherwise, likely a Godot bug.
+ blonk = false
+ blink()
+ timer.start(0.6)
+ RenderingServer.canvas_item_clear(surface)
+ if has_focus():
+ var char_size := code_font.get_char_size(69,
+ get_theme_font_size(&"TextEdit", &"font_size"))
+ for caret in get_caret_count():
+ var caret_draw_pos := Vector2(get_rect_at_line_column(
+ get_caret_line(caret), get_caret_column(caret)).end) + Vector2(1, -2)
+ if is_overtype_mode_enabled():
+ RenderingServer.canvas_item_add_line(surface, caret_draw_pos,
+ caret_draw_pos + Vector2(char_size.x, 0), caret_color, 1)
+ else:
+ RenderingServer.canvas_item_add_line(surface, caret_draw_pos,
+ caret_draw_pos + Vector2(0, -char_size.y - 1), caret_color, 1)
+
+var blonk := true
+func blink() -> void:
+ blonk = not blonk
+ RenderingServer.canvas_item_set_visible(surface, blonk)
+
+func _on_focus_entered() -> void:
+ timer.start(0.6)
+
+func _on_focus_exited() -> void:
+ timer.stop()
+ RenderingServer.canvas_item_clear(surface)
+
+func _on_mouse_exited() -> void:
+ hovered = false
+ queue_redraw()
+
+func _draw() -> void:
+ if editable and hovered and hover_stylebox != null:
+ draw_style_box(hover_stylebox, Rect2(Vector2.ZERO, size))
+
+func _input(event: InputEvent) -> void:
+ if (has_focus() and event is InputEventMouseButton and\
+ not get_global_rect().has_point(event.position)):
+ release_focus()
+
+func _gui_input(event: InputEvent) -> void:
+ mouse_filter = Utils.mouse_filter_pass_non_drag_events(event)
+
+ if event is InputEventMouseMotion and event.button_mask == 0:
+ hovered = true
+ queue_redraw()
+ if event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_RIGHT:
+ grab_focus()
+ var context_popup := ContextPopup.instantiate()
+ var btn_arr: Array[Button] = [
+ Utils.create_btn(tr(&"#undo"), undo, !has_undo()),
+ Utils.create_btn(tr(&"#redo"), redo, !has_redo()),
+ Utils.create_btn(tr(&"#copy"), copy, text.is_empty()),
+ Utils.create_btn(tr(&"#paste"), paste, !DisplayServer.clipboard_has()),
+ Utils.create_btn(tr(&"#cut"), cut, text.is_empty()),
+ ]
+
+ add_child(context_popup)
+ context_popup.set_button_array(btn_arr, true, 72)
+ var viewport := get_viewport()
+ Utils.popup_under_pos(context_popup, viewport.get_mouse_position(), viewport)
+ accept_event()
+ else:
+ # Set these inputs as handled, so the default UndoRedo doesn't eat them.
+ if event.is_action_pressed(&"redo"):
+ if has_redo():
+ redo()
+ accept_event()
+ elif event.is_action_pressed(&"undo"):
+ if has_undo():
+ undo()
+ accept_event()
+
+
+# I'd prefer to block non-ASCII inputs in SVG code. SVG syntax is ASCII-only, and while
+# text blocks and comments allow non-ASCII, they are still difficult to deal with
+# because they are 2-4 bytes long. tags make the situation a whole lot harder,
+# but for now they are not supported. Maybe in some future version I'll have them
+# be translated directly into paths or have an abstraction over them, I don't know.
+# Either way, not planning to support UTF-8, so I block it if the user tries to type it.
+func _handle_unicode_input(unicode_char: int, caret_index: int) -> void:
+ if (block_non_ascii and unicode_char <= 127) or not block_non_ascii:
+ insert_text_at_caret(char(unicode_char), caret_index)
diff --git a/src/ui_elements/BetterToggleButton.gd b/src/ui_elements/BetterToggleButton.gd
new file mode 100644
index 000000000..5842394c1
--- /dev/null
+++ b/src/ui_elements/BetterToggleButton.gd
@@ -0,0 +1,30 @@
+## A regular Button that overlays a stylebox when hovered while pressed.
+class_name BetterToggleButton extends Button
+
+var hovered := false
+
+# Overlayed on top when the Button is hovered while pressed.
+@export var hover_pressed_stylebox: StyleBox
+@export var hover_pressed_font_color: Color
+
+func _ready() -> void:
+ mouse_entered.connect(_on_mouse_entered)
+ mouse_exited.connect(_on_mouse_exited)
+ add_theme_color_override(&"font_hover_color", get_theme_color(
+ &"font_hover_color", &"Button").blend(hover_pressed_font_color))
+
+func _on_mouse_entered() -> void:
+ hovered = true
+ if not disabled and hover_pressed_font_color != Color.BLACK:
+ add_theme_color_override(&"font_pressed_color", get_theme_color(
+ &"font_pressed_color", &"Button").blend(hover_pressed_font_color))
+ queue_redraw()
+
+func _on_mouse_exited() -> void:
+ hovered = false
+ remove_theme_color_override(&"font_pressed_color")
+ queue_redraw()
+
+func _draw() -> void:
+ if not disabled and button_pressed and hovered and hover_pressed_stylebox != null:
+ draw_style_box(hover_pressed_stylebox, Rect2(Vector2.ZERO, size))
diff --git a/src/ui_elements/GridDrawingControl.gd b/src/ui_elements/GridDrawingControl.gd
new file mode 100644
index 000000000..35f5ff194
--- /dev/null
+++ b/src/ui_elements/GridDrawingControl.gd
@@ -0,0 +1,16 @@
+class_name PanelGrid extends GridContainer
+
+const font = preload("res://visual/fonts/Font.ttf")
+
+@export var items: Array[String]
+@export var stylebox: StyleBox
+
+func setup() -> void:
+ for item in items:
+ var panel_container := PanelContainer.new()
+ panel_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ panel_container.add_theme_stylebox_override(&"panel", stylebox)
+ var label := Label.new()
+ label.text = item
+ panel_container.add_child(label)
+ add_child(panel_container)
diff --git a/src/ui_elements/color_edit.gd b/src/ui_elements/color_edit.gd
new file mode 100644
index 000000000..06a101601
--- /dev/null
+++ b/src/ui_elements/color_edit.gd
@@ -0,0 +1,83 @@
+## A color editor, not tied to any attribute.
+extends HBoxContainer
+
+const ColorPopup = preload("res://src/ui_elements/color_popup.tscn")
+const ColorPickerPopup = preload("res://src/ui_elements/color_picker_popup.tscn")
+const checkerboard = preload("res://visual/icons/backgrounds/ColorButtonBG.svg")
+
+@onready var color_button: Button = $Button
+@onready var color_edit: LineEdit = $LineEdit
+@onready var color_picker: Popup
+
+@export var enable_palettes := true
+
+signal value_changed(new_value: String)
+var value: String:
+ set(new_value):
+ new_value = ColorParser.add_hash_if_hex(new_value)
+ if ColorParser.is_valid_hex(new_value) or ColorParser.is_valid_named(new_value) or\
+ ColorParser.is_valid_rgb(new_value):
+ new_value = new_value.trim_prefix("#")
+ if new_value != value:
+ value = new_value
+ value_changed.emit(value)
+ sync(value)
+
+
+func _ready() -> void:
+ sync(value)
+
+func is_color_valid_non_url(https://github.com/new_value:%20String) -> bool:
+ new_value = ColorParser.add_hash_if_hex(new_value)
+ return ColorParser.is_valid_named(new_value) or\
+ ColorParser.is_valid_hex(new_value) or ColorParser.is_valid_rgb(new_value)
+
+func sync(new_value: String) -> void:
+ color_edit.remove_theme_color_override(&"font_color")
+ color_edit.text = new_value.trim_prefix("#")
+ queue_redraw()
+
+func _on_button_pressed() -> void:
+ if enable_palettes:
+ color_picker = ColorPopup.instantiate()
+ else:
+ color_picker = ColorPickerPopup.instantiate()
+ color_picker.current_value = ColorParser.add_hash_if_hex(value)
+ add_child(color_picker)
+ color_picker.color_picked.connect(_on_color_picked)
+ Utils.popup_under_rect(color_picker, color_edit.get_global_rect(), get_viewport())
+
+func _draw() -> void:
+ var button_size := color_button.get_size()
+ var line_edit_size := color_edit.get_size()
+ draw_set_transform(Vector2(line_edit_size.x, 1))
+ var stylebox := StyleBoxFlat.new()
+ stylebox.corner_radius_top_right = 5
+ stylebox.corner_radius_bottom_right = 5
+ stylebox.bg_color = ColorParser.string_to_color(ColorParser.add_hash_if_hex(value))
+ draw_texture(checkerboard, Vector2.ZERO)
+ draw_style_box(stylebox, Rect2(Vector2.ZERO, button_size - Vector2(1, 2)))
+
+
+func _on_text_submitted(new_text: String) -> void:
+ value = new_text
+
+func _on_text_change_canceled() -> void:
+ sync(value)
+
+func _on_color_picked(new_color: String, close_picker: bool) -> void:
+ value = new_color
+ if close_picker:
+ color_picker.queue_free()
+
+
+func _on_button_resized() -> void:
+ # Not sure why this is needed, but the button doesn't have a correct size at first
+ # which screws with the drawing logic.
+ queue_redraw()
+
+func _on_line_edit_text_changed(new_text: String) -> void:
+ if is_color_valid_non_url(https://github.com/new_text):
+ color_edit.add_theme_color_override(&"font_color", Color(0.6, 1.0, 0.6))
+ else:
+ color_edit.add_theme_color_override(&"font_color", Color(1.0, 0.6, 0.6))
diff --git a/src/ui_elements/color_edit.tscn b/src/ui_elements/color_edit.tscn
new file mode 100644
index 000000000..38b4e5f97
--- /dev/null
+++ b/src/ui_elements/color_edit.tscn
@@ -0,0 +1,54 @@
+[gd_scene load_steps=5 format=3 uid="uid://5f8uxavn1or1"]
+
+[ext_resource type="Script" path="res://src/ui_elements/color_edit.gd" id="1_1uexr"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="1_efrfl"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q6tej"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_edu3w"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[node name="ColorEdit" type="HBoxContainer"]
+custom_minimum_size = Vector2(0, 22)
+offset_right = 50.0
+offset_bottom = 21.0
+theme_override_constants/separation = 0
+script = ExtResource("1_1uexr")
+
+[node name="LineEdit" type="LineEdit" parent="."]
+custom_minimum_size = Vector2(54, 0)
+layout_mode = 2
+focus_mode = 1
+theme_type_variation = &"RightConnectedLineEdit"
+script = ExtResource("1_efrfl")
+hover_stylebox = SubResource("StyleBoxFlat_q6tej")
+focus_stylebox = SubResource("StyleBoxFlat_edu3w")
+code_font_tooltip = true
+
+[node name="Button" type="Button" parent="."]
+custom_minimum_size = Vector2(13, 0)
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"LeftConnectedButtonTransparent"
+
+[connection signal="text_change_canceled" from="LineEdit" to="." method="_on_text_change_canceled"]
+[connection signal="text_changed" from="LineEdit" to="." method="_on_line_edit_text_changed"]
+[connection signal="text_submitted" from="LineEdit" to="." method="_on_text_submitted"]
+[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
+[connection signal="resized" from="Button" to="." method="_on_button_resized"]
diff --git a/src/ui_elements/color_field.gd b/src/ui_elements/color_field.gd
new file mode 100644
index 000000000..75a073953
--- /dev/null
+++ b/src/ui_elements/color_field.gd
@@ -0,0 +1,116 @@
+## An editor to be tied to a color attribute.
+extends HBoxContainer
+
+signal focused
+var attribute: AttributeColor
+var attribute_name: String
+
+const ColorPopup = preload("res://src/ui_elements/color_popup.tscn")
+const checkerboard = preload("res://visual/icons/backgrounds/ColorButtonBG.svg")
+
+@onready var color_button: Button = $Button
+@onready var color_edit: LineEdit = $LineEdit
+@onready var color_popup: Popup
+
+func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+ # Validate the value.
+ if not is_valid(new_value):
+ sync(attribute.get_value())
+ return
+
+ new_value = ColorParser.add_hash_if_hex(new_value)
+ if ColorParser.are_colors_same(new_value, attribute.default):
+ new_value = attribute.default
+
+ sync(attribute.autoformat(new_value))
+ # Update the attribute.
+ if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
+ match update_type:
+ Utils.UpdateType.INTERMEDIATE:
+ attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
+ Utils.UpdateType.FINAL:
+ attribute.set_value(new_value, Attribute.SyncMode.FINAL)
+ _:
+ attribute.set_value(new_value)
+
+
+func _ready() -> void:
+ set_value(attribute.get_value())
+ attribute.value_changed.connect(set_value)
+ color_edit.tooltip_text = attribute_name
+
+
+func _on_button_pressed() -> void:
+ color_popup = ColorPopup.instantiate()
+ color_popup.current_value = attribute.get_value()
+ add_child(color_popup)
+ color_popup.color_picked.connect(_on_color_picked)
+ Utils.popup_under_rect(color_popup, color_edit.get_global_rect(), get_viewport())
+
+func _draw() -> void:
+ var button_size := color_button.get_size()
+ var line_edit_size := color_edit.get_size()
+ draw_set_transform(Vector2(line_edit_size.x, 1))
+ var stylebox := StyleBoxFlat.new()
+ stylebox.corner_radius_top_right = 5
+ stylebox.corner_radius_bottom_right = 5
+ stylebox.bg_color = ColorParser.string_to_color(attribute.get_value())
+ draw_texture(checkerboard, Vector2.ZERO)
+ draw_style_box(stylebox, Rect2(Vector2.ZERO, button_size - Vector2(1, 2)))
+
+
+func _on_focus_entered() -> void:
+ color_edit.remove_theme_color_override(&"font_color")
+ focused.emit()
+
+func _on_text_submitted(new_text: String) -> void:
+ if new_text.strip_edges().is_empty():
+ set_value(attribute.default)
+ else:
+ set_value(new_text)
+
+func _on_text_change_canceled() -> void:
+ sync(attribute.get_value())
+
+
+func _on_color_picked(new_color: String, close_picker: bool) -> void:
+ if close_picker:
+ color_popup.queue_free()
+ set_value(new_color, Utils.UpdateType.FINAL)
+ else:
+ set_value(new_color, Utils.UpdateType.INTERMEDIATE)
+
+func is_valid(text: String) -> bool:
+ return ColorParser.is_valid(ColorParser.add_hash_if_hex(text))
+
+
+func _on_button_resized() -> void:
+ # Not sure why this is needed, but the button doesn't have a correct size at first
+ # which screws with the drawing logic.
+ queue_redraw()
+
+func _on_text_changed(new_text: String) -> void:
+ if is_valid(new_text):
+ color_edit.add_theme_color_override(&"font_color", Color(0.6, 1.0, 0.6))
+ else:
+ color_edit.add_theme_color_override(&"font_color", Color(1.0, 0.6, 0.6))
+
+func sync(new_value: String) -> void:
+ if color_edit != null:
+ if new_value == attribute.default:
+ color_edit.add_theme_color_override(&"font_color", Color(0.64, 0.64, 0.64))
+ else:
+ color_edit.remove_theme_color_override(&"font_color")
+ color_edit.text = new_value.trim_prefix("#")
+ queue_redraw()
+
+
+func _on_button_gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and\
+ event.is_pressed():
+ accept_event()
+ var mouse_motion_event := InputEventMouseMotion.new()
+ mouse_motion_event.position = get_viewport().get_mouse_position()
+ Input.parse_input_event(mouse_motion_event)
+ else:
+ color_button.mouse_filter = Utils.mouse_filter_pass_non_drag_events(event)
diff --git a/src/ui_elements/color_field.tscn b/src/ui_elements/color_field.tscn
new file mode 100644
index 000000000..fcb270a29
--- /dev/null
+++ b/src/ui_elements/color_field.tscn
@@ -0,0 +1,57 @@
+[gd_scene load_steps=5 format=3 uid="uid://carf2o1y7wvmc"]
+
+[ext_resource type="Script" path="res://src/ui_elements/color_field.gd" id="1_2pe1j"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="3_u777p"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q6tej"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_edu3w"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[node name="ColorField" type="HBoxContainer"]
+custom_minimum_size = Vector2(0, 22)
+offset_right = 50.0
+offset_bottom = 21.0
+theme_override_constants/separation = 0
+script = ExtResource("1_2pe1j")
+
+[node name="LineEdit" type="LineEdit" parent="."]
+custom_minimum_size = Vector2(54, 0)
+layout_mode = 2
+focus_mode = 1
+theme_type_variation = &"RightConnectedLineEdit"
+script = ExtResource("3_u777p")
+hover_stylebox = SubResource("StyleBoxFlat_q6tej")
+focus_stylebox = SubResource("StyleBoxFlat_edu3w")
+code_font_tooltip = true
+
+[node name="Button" type="Button" parent="."]
+custom_minimum_size = Vector2(13, 0)
+layout_mode = 2
+focus_mode = 0
+mouse_filter = 1
+mouse_default_cursor_shape = 2
+theme_type_variation = &"LeftConnectedButtonTransparent"
+
+[connection signal="focus_entered" from="LineEdit" to="." method="_on_focus_entered"]
+[connection signal="text_change_canceled" from="LineEdit" to="." method="_on_text_change_canceled"]
+[connection signal="text_changed" from="LineEdit" to="." method="_on_text_changed"]
+[connection signal="text_submitted" from="LineEdit" to="." method="_on_text_submitted"]
+[connection signal="gui_input" from="Button" to="." method="_on_button_gui_input"]
+[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
+[connection signal="resized" from="Button" to="." method="_on_button_resized"]
diff --git a/src/ui_elements/color_picker_popup.gd b/src/ui_elements/color_picker_popup.gd
new file mode 100644
index 000000000..8d5490f8e
--- /dev/null
+++ b/src/ui_elements/color_picker_popup.gd
@@ -0,0 +1,19 @@
+extends Popup
+
+const GoodColorPickerType = preload("res://src/ui_elements/good_color_picker.gd")
+
+@onready var picker: GoodColorPickerType = $PanelContainer/MarginContainer/ColorPicker
+
+signal color_picked(new_color: String, final: bool)
+var current_value: String
+
+func _ready() -> void:
+ await get_tree().process_frame
+ picker.setup_color(current_value)
+
+func pick_color(color: String) -> void:
+ color_picked.emit(color, false)
+
+
+func _on_popup_hide() -> void:
+ queue_free()
diff --git a/src/ui_elements/color_picker_popup.tscn b/src/ui_elements/color_picker_popup.tscn
new file mode 100644
index 000000000..f21191ba8
--- /dev/null
+++ b/src/ui_elements/color_picker_popup.tscn
@@ -0,0 +1,27 @@
+[gd_scene load_steps=3 format=3 uid="uid://bs68u5annwepo"]
+
+[ext_resource type="Script" path="res://src/ui_elements/color_picker_popup.gd" id="1_wmbff"]
+[ext_resource type="PackedScene" uid="uid://b1eig44cov474" path="res://src/ui_elements/good_color_picker.tscn" id="2_jafm6"]
+
+[node name="ColorPickerPopup" type="Popup"]
+transparent_bg = true
+size = Vector2i(218, 322)
+visible = true
+script = ExtResource("1_wmbff")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+offset_right = 4.0
+offset_bottom = 4.0
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 8
+theme_override_constants/margin_bottom = 8
+
+[node name="ColorPicker" parent="PanelContainer/MarginContainer" instance=ExtResource("2_jafm6")]
+layout_mode = 2
+
+[connection signal="popup_hide" from="." to="." method="_on_popup_hide"]
+[connection signal="color_changed" from="PanelContainer/MarginContainer/ColorPicker" to="." method="pick_color"]
diff --git a/src/ui_elements/color_popup.gd b/src/ui_elements/color_popup.gd
new file mode 100644
index 000000000..781887b66
--- /dev/null
+++ b/src/ui_elements/color_popup.gd
@@ -0,0 +1,106 @@
+## A popup for picking a color.
+extends Popup
+
+const GoodColorPickerType = preload("res://src/ui_elements/good_color_picker.gd")
+const ColorSwatchType = preload("res://src/ui_elements/color_swatch.gd")
+
+const ColorSwatch = preload("res://src/ui_elements/color_swatch.tscn")
+
+signal color_picked(new_color: String, final: bool)
+var current_value: String
+
+var palette_mode := true
+
+@onready var palettes_content: ScrollContainer = %Content/Palettes
+@onready var palettes_content_container: VBoxContainer = %PalettesContent
+@onready var search_field: BetterLineEdit = %SearchBox/SearchField
+@onready var color_picker_content: VBoxContainer = %Content/ColorPicker
+@onready var color_picker: GoodColorPickerType = %Content/ColorPicker
+@onready var switch_mode_button: Button = $PanelContainer/MainContainer/SwitchMode
+@onready var panel_container: PanelContainer = $PanelContainer
+
+var swatches_list: Array[ColorSwatchType] = [] # Updated manually.
+
+func _ready() -> void:
+ # Setup the switch mode button.
+ for theme_type in [&"normal", &"hover", &"pressed"]:
+ var sb: StyleBoxFlat = switch_mode_button.get_theme_stylebox(theme_type,
+ &"TranslucentButton").duplicate()
+ sb.corner_radius_top_left = 0
+ sb.corner_radius_top_right = 0
+ sb.corner_radius_bottom_left = 4
+ sb.corner_radius_bottom_right = 4
+ sb.content_margin_bottom = 3
+ sb.content_margin_top = 3
+ switch_mode_button.add_theme_stylebox_override(theme_type, sb)
+ # Setup the rest.
+ update_palettes()
+ update_color_picker()
+
+func update_palettes(search_text := "") -> void:
+ for child in palettes_content_container.get_children():
+ child.queue_free()
+ search_field.placeholder_text = tr(&"#search_color")
+ var reserved_color_palette := ColorPalette.new("", [NamedColor.new("none")])
+ # TODO Gradients should be added here.
+ var displayed_palettes: Array[ColorPalette] = [reserved_color_palette]
+ displayed_palettes += GlobalSettings.get_palettes()
+ for palette in displayed_palettes:
+ var colors_to_show: Array[NamedColor] = []
+ for named_color in palette.named_colors:
+ if search_text.is_empty() or search_text.is_subsequence_ofn(named_color.name):
+ colors_to_show.append(named_color)
+
+ if colors_to_show.is_empty():
+ continue
+
+ var palette_container := VBoxContainer.new()
+ # Only the reserved palette should have an empty name.
+ if not palette.name.is_empty():
+ var palette_label := Label.new()
+ palette_label.text = palette.name
+ palette_label.add_theme_font_size_override(&"font_size", 15)
+ palette_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ palette_container.add_child(palette_label)
+
+ var swatch_container := HFlowContainer.new()
+ swatch_container.add_theme_constant_override(&"h_separation", 3)
+ for named_color in colors_to_show:
+ var swatch := ColorSwatch.instantiate()
+ swatch.named_color = named_color
+ swatch.pressed.connect(pick_palette_color.bind(named_color.color))
+ swatch_container.add_child(swatch)
+ swatches_list.append(swatch)
+ if ColorParser.are_colors_same("#" + swatch.named_color.color, current_value):
+ swatch.disabled = true
+ swatch.mouse_default_cursor_shape = Control.CURSOR_ARROW
+ palette_container.add_child(swatch_container)
+ palettes_content_container.add_child(palette_container)
+
+func update_color_picker() -> void:
+ color_picker.setup_color(current_value)
+
+func pick_palette_color(color: String) -> void:
+ color_picked.emit(color, true)
+
+func pick_color(color: String) -> void:
+ current_value = color
+ update_palettes(search_field.text)
+ color_picked.emit(color, false)
+
+
+# Switching between palette mode and color picker mode.
+func _switch_mode() -> void:
+ palette_mode = not palette_mode
+ switch_mode_button.text = tr(&"#palettes" if palette_mode else &"#color_picker")
+ color_picker_content.visible = not palette_mode
+ palettes_content.visible = palette_mode
+
+
+func _on_popup_hide() -> void:
+ color_picked.emit(current_value, true)
+ queue_free()
+
+
+func _on_search_field_text_changed(new_text: String) -> void:
+ update_palettes(new_text)
diff --git a/src/ui_elements/color_popup.tscn b/src/ui_elements/color_popup.tscn
new file mode 100644
index 000000000..f88532411
--- /dev/null
+++ b/src/ui_elements/color_popup.tscn
@@ -0,0 +1,104 @@
+[gd_scene load_steps=7 format=3 uid="uid://f5cljfdpe85v"]
+
+[ext_resource type="Script" path="res://src/ui_elements/color_popup.gd" id="1_t1mgf"]
+[ext_resource type="FontFile" uid="uid://clpf84p1lfwlp" path="res://visual/fonts/Font.ttf" id="2_5tod5"]
+[ext_resource type="PackedScene" uid="uid://b1eig44cov474" path="res://src/ui_elements/good_color_picker.tscn" id="2_jv3ea"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="2_lkukd"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jo0is"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ea0ux"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="ColorPopup" type="Popup"]
+transparent_bg = true
+size = Vector2i(218, 347)
+visible = true
+script = ExtResource("1_t1mgf")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+custom_minimum_size = Vector2(160, 0)
+offset_right = 218.0
+offset_bottom = 43.0
+
+[node name="MainContainer" type="VBoxContainer" parent="PanelContainer"]
+layout_mode = 2
+
+[node name="Content" type="MarginContainer" parent="PanelContainer/MainContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(214, 0)
+layout_mode = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 8
+theme_override_constants/margin_bottom = 2
+
+[node name="Palettes" type="ScrollContainer" parent="PanelContainer/MainContainer/Content"]
+custom_minimum_size = Vector2(0, 303)
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MainContainer/Content/Palettes"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 12
+
+[node name="SearchBox" type="MarginContainer" parent="PanelContainer/MainContainer/Content/Palettes/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/margin_left = 12
+theme_override_constants/margin_right = 12
+
+[node name="SearchField" type="LineEdit" parent="PanelContainer/MainContainer/Content/Palettes/VBoxContainer/SearchBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_fonts/font = ExtResource("2_5tod5")
+script = ExtResource("2_lkukd")
+hover_stylebox = SubResource("StyleBoxFlat_jo0is")
+focus_stylebox = SubResource("StyleBoxFlat_ea0ux")
+
+[node name="PalettesContent" type="VBoxContainer" parent="PanelContainer/MainContainer/Content/Palettes/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 7
+
+[node name="ColorPicker" parent="PanelContainer/MainContainer/Content" instance=ExtResource("2_jv3ea")]
+visible = false
+layout_mode = 2
+
+[node name="SwitchMode" type="Button" parent="PanelContainer/MainContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_override_font_sizes/font_size = 13
+text = "#palettes"
+
+[node name="Label" type="Label" parent="."]
+offset_right = 40.0
+offset_bottom = 22.0
+
+[connection signal="popup_hide" from="." to="." method="_on_popup_hide"]
+[connection signal="text_changed" from="PanelContainer/MainContainer/Content/Palettes/VBoxContainer/SearchBox/SearchField" to="." method="_on_search_field_text_changed"]
+[connection signal="color_changed" from="PanelContainer/MainContainer/Content/ColorPicker" to="." method="pick_color"]
+[connection signal="pressed" from="PanelContainer/MainContainer/SwitchMode" to="." method="_switch_mode"]
diff --git a/src/ui_elements/color_swatch.gd b/src/ui_elements/color_swatch.gd
new file mode 100644
index 000000000..e74526f16
--- /dev/null
+++ b/src/ui_elements/color_swatch.gd
@@ -0,0 +1,74 @@
+extends Button
+
+const code_font = preload("res://visual/fonts/FontMono.ttf")
+const checkerboard = preload("res://visual/icons/backgrounds/Checkerboard.svg")
+const plus_icon = preload("res://visual/icons/Plus.svg")
+const gear_icon = preload("res://visual/icons/GearOutlined.svg")
+
+const ColorSwatch = preload("res://src/ui_elements/color_swatch.tscn")
+
+enum Type {CHOOSE_COLOR, CONFIGURE_COLOR, ADD_COLOR}
+var type := Type.CHOOSE_COLOR
+
+var named_color: NamedColor
+
+func _ready() -> void:
+ if type == Type.ADD_COLOR:
+ tooltip_text = tr(&"#add_color")
+
+func _draw() -> void:
+ if type == Type.ADD_COLOR:
+ plus_icon.draw(get_canvas_item(), (size - plus_icon.get_size()) / 2)
+ return
+
+ var color := Color.from_string(named_color.color, Color(0, 0, 0))
+ var bounds := Vector2(2, 2)
+ if color.a != 1 or named_color.color == "none":
+ draw_texture_rect(checkerboard, Rect2(bounds, size - bounds * 2), false)
+ if named_color.color != "none":
+ draw_rect(Rect2(bounds, size - bounds * 2), color)
+ if type == Type.CONFIGURE_COLOR and is_hovered():
+ gear_icon.draw(get_canvas_item(), (size - gear_icon.get_size()) / 2)
+
+func _make_custom_tooltip(_for_text: String) -> Object:
+ if type == Type.ADD_COLOR:
+ return null
+ elif type == Type.CHOOSE_COLOR or type == Type.CONFIGURE_COLOR:
+ var rtl := RichTextLabel.new()
+ rtl.autowrap_mode = TextServer.AUTOWRAP_OFF
+ rtl.fit_content = true
+ rtl.bbcode_enabled = true
+ rtl.add_theme_font_override(&"mono_font", code_font)
+ # Set up the text.
+ if not named_color.name.is_empty():
+ rtl.add_text(named_color.name)
+ rtl.newline()
+ rtl.push_mono()
+ if named_color.color == "none":
+ rtl.add_text("none")
+ else:
+ rtl.add_text("#" + named_color.color)
+ return rtl
+ return null
+
+func _get_drag_data(_at_position: Vector2) -> Variant:
+ if type == Type.CONFIGURE_COLOR:
+ var data: NamedColor = named_color
+ # Set up a preview.
+ var preview := ColorSwatch.instantiate()
+ preview.named_color = named_color
+ preview.modulate = Color(1, 1, 1, 0.85)
+ set_drag_preview(preview)
+ return data
+ return null
+
+
+# For configuration swatches.
+func change_color_name(new_name: String) -> void:
+ named_color.name = new_name
+ GlobalSettings.save_user_data()
+
+func change_color(new_color: String) -> void:
+ named_color.color = new_color
+ GlobalSettings.save_user_data()
+ queue_redraw()
diff --git a/src/ui_elements/color_swatch.tscn b/src/ui_elements/color_swatch.tscn
new file mode 100644
index 000000000..498f5060e
--- /dev/null
+++ b/src/ui_elements/color_swatch.tscn
@@ -0,0 +1,37 @@
+[gd_scene load_steps=5 format=3 uid="uid://cpvtf3kaa2ltr"]
+
+[ext_resource type="Script" path="res://src/ui_elements/color_swatch.gd" id="1_2b2yq"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_m3oh3"]
+bg_color = Color(0.164706, 0.188235, 0.301961, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kojaf"]
+bg_color = Color(0.247059, 0.313726, 0.45098, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ym6fi"]
+bg_color = Color(0.34902, 0.52549, 0.701961, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[node name="ColorSwatch" type="Button"]
+custom_minimum_size = Vector2(22, 22)
+offset_right = 20.0
+offset_bottom = 20.0
+focus_mode = 0
+mouse_filter = 1
+mouse_default_cursor_shape = 2
+theme_override_styles/normal = SubResource("StyleBoxFlat_m3oh3")
+theme_override_styles/hover = SubResource("StyleBoxFlat_kojaf")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_ym6fi")
+theme_override_styles/disabled = SubResource("StyleBoxFlat_ym6fi")
+script = ExtResource("1_2b2yq")
diff --git a/src/ui_elements/context_popup.gd b/src/ui_elements/context_popup.gd
new file mode 100644
index 000000000..634e81d27
--- /dev/null
+++ b/src/ui_elements/context_popup.gd
@@ -0,0 +1,39 @@
+## The standard context menu popup.
+extends Popup
+
+@onready var panel: PanelContainer = $PanelContainer
+@onready var main_container: VBoxContainer = $PanelContainer/MainContainer
+
+func add_button(btn: Button, align_left: bool, should_reset_size := true) -> void:
+ if not btn is CheckBox:
+ btn.theme_type_variation = &"FlatButton"
+ btn.pressed.connect(queue_free)
+ btn.focus_mode = Control.FOCUS_NONE
+ main_container.add_child(btn)
+ if align_left:
+ btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
+ if should_reset_size:
+ reset_size()
+
+func set_button_array(buttons: Array[Button], align_left := false,
+min_width := -1) -> void:
+ for button in main_container.get_children():
+ button.free()
+ if buttons.is_empty():
+ return
+ else:
+ var last_button_idx := buttons.size() - 1
+ for i in last_button_idx:
+ add_button(buttons[i], align_left, false)
+ add_button(buttons[last_button_idx], align_left)
+ if min_width > 0:
+ min_size.x = ceili(min_width)
+ panel.custom_minimum_size.x = min_width
+
+
+func get_button_count() -> int:
+ return main_container.get_child_count()
+
+
+func _on_popup_hide() -> void:
+ queue_free()
diff --git a/src/ui_elements/context_popup.tscn b/src/ui_elements/context_popup.tscn
new file mode 100644
index 000000000..26d475a3b
--- /dev/null
+++ b/src/ui_elements/context_popup.tscn
@@ -0,0 +1,22 @@
+[gd_scene load_steps=2 format=3 uid="uid://wp77eqhikp6k"]
+
+[ext_resource type="Script" path="res://src/ui_elements/context_popup.gd" id="1_d45ly"]
+
+[node name="ContextPopup" type="Popup"]
+disable_3d = true
+transparent_bg = true
+size = Vector2i(4, 4)
+visible = true
+script = ExtResource("1_d45ly")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+offset_right = 4.0
+offset_bottom = 4.0
+
+[node name="MainContainer" type="VBoxContainer" parent="PanelContainer"]
+unique_name_in_owner = true
+clip_contents = true
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[connection signal="popup_hide" from="." to="." method="_on_popup_hide"]
diff --git a/src/ui_elements/dropdown.gd b/src/ui_elements/dropdown.gd
new file mode 100644
index 000000000..a13c69502
--- /dev/null
+++ b/src/ui_elements/dropdown.gd
@@ -0,0 +1,61 @@
+## A dropdown with multiple options, not tied to any attribute.
+extends HBoxContainer
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+
+signal value_changed(new_value: String)
+
+@onready var line_edit: BetterLineEdit = $LineEdit
+
+@export var values: Array[String]
+@export var restricted := true
+
+var value := "":
+ set(new_value):
+ if value != new_value:
+ value = new_value
+ value_changed.emit(value)
+ if line_edit != null:
+ line_edit.text = value
+
+func _ready() -> void:
+ if not values.is_empty():
+ value = values[0]
+
+ var max_length := 0
+ for val in values:
+ max_length = maxi(val.length(), max_length)
+
+ line_edit.custom_minimum_size.x = line_edit.get_theme_font(&"font").get_string_size(
+ "m".repeat(max_length + 1), HORIZONTAL_ALIGNMENT_LEFT, -1,
+ line_edit.get_theme_font_size(&"font_size")).x
+ line_edit.size.x = 0
+
+func _on_button_pressed() -> void:
+ var btn_arr: Array[Button] = []
+ for val in values:
+ btn_arr.append(Utils.create_btn(val, _on_value_chosen.bind(val), val == value))
+
+ var value_picker := ContextPopup.instantiate()
+ add_child(value_picker)
+ value_picker.set_button_array(btn_arr, false, size.x)
+ Utils.popup_under_rect(value_picker, line_edit.get_global_rect(), get_viewport())
+
+func _on_value_chosen(new_value: String) -> void:
+ value = new_value
+
+
+func _on_text_submitted(new_text: String) -> void:
+ if (restricted and new_text in values) or not restricted:
+ value = new_text
+ else:
+ line_edit.text = value
+ line_edit.remove_theme_color_override(&"font_color")
+
+
+func _on_text_changed(new_text: String) -> void:
+ if restricted:
+ if new_text in values:
+ line_edit.add_theme_color_override(&"font_color", Color(0.6, 1.0, 0.6))
+ else:
+ line_edit.add_theme_color_override(&"font_color", Color(1.0, 0.6, 0.6))
diff --git a/src/ui_elements/dropdown.tscn b/src/ui_elements/dropdown.tscn
new file mode 100644
index 000000000..2ac19153f
--- /dev/null
+++ b/src/ui_elements/dropdown.tscn
@@ -0,0 +1,55 @@
+[gd_scene load_steps=6 format=3 uid="uid://dbu1lvajypafb"]
+
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="1_0ifbb"]
+[ext_resource type="Script" path="res://src/ui_elements/dropdown.gd" id="1_133xu"]
+[ext_resource type="Texture2D" uid="uid://coda6chhcatal" path="res://visual/icons/Arrow.svg" id="2_4oygd"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q1iin"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uaomi"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[node name="Dropdown" type="HBoxContainer"]
+custom_minimum_size = Vector2(0, 22)
+offset_right = 49.0
+offset_bottom = 22.0
+size_flags_horizontal = 0
+size_flags_vertical = 0
+theme_override_constants/separation = 0
+script = ExtResource("1_133xu")
+
+[node name="LineEdit" type="LineEdit" parent="."]
+layout_mode = 2
+focus_mode = 1
+theme_type_variation = &"RightConnectedLineEdit"
+script = ExtResource("1_0ifbb")
+hover_stylebox = SubResource("StyleBoxFlat_q1iin")
+focus_stylebox = SubResource("StyleBoxFlat_uaomi")
+
+[node name="Button" type="Button" parent="."]
+custom_minimum_size = Vector2(15, 0)
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"LeftConnectedButton"
+icon = ExtResource("2_4oygd")
+expand_icon = true
+
+[connection signal="text_changed" from="LineEdit" to="." method="_on_text_changed"]
+[connection signal="text_submitted" from="LineEdit" to="." method="_on_text_submitted"]
+[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
diff --git a/src/ui_elements/enum_field.gd b/src/ui_elements/enum_field.gd
new file mode 100644
index 000000000..71ea88204
--- /dev/null
+++ b/src/ui_elements/enum_field.gd
@@ -0,0 +1,87 @@
+## An editor to be tied to an AttributeEnum.
+extends HBoxContainer
+
+signal focused
+var attribute: AttributeEnum
+var attribute_name: String
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const bold_font = preload("res://visual/fonts/FontBold.ttf")
+
+@onready var indicator: LineEdit = $LineEdit
+@onready var button: Button = $Button
+
+func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+ sync(attribute.autoformat(new_value))
+ if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
+ match update_type:
+ Utils.UpdateType.INTERMEDIATE:
+ attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
+ Utils.UpdateType.FINAL:
+ attribute.set_value(new_value, Attribute.SyncMode.FINAL)
+ _:
+ attribute.set_value(new_value)
+
+
+func _ready() -> void:
+ set_value(attribute.get_value())
+ indicator.tooltip_text = attribute_name
+
+func _on_button_pressed() -> void:
+ var value_picker := ContextPopup.instantiate()
+ var btn_arr: Array[Button] = []
+ for enum_constant in attribute.possible_values:
+ var btn := Utils.create_btn(enum_constant, _on_option_pressed.bind(enum_constant),
+ enum_constant == attribute.get_value())
+ if enum_constant == attribute.default:
+ btn.add_theme_font_override(&"font", bold_font)
+ btn_arr.append(btn)
+ add_child(value_picker)
+ value_picker.set_button_array(btn_arr, false, size.x)
+ Utils.popup_under_rect(value_picker, indicator.get_global_rect(), get_viewport())
+
+func _on_option_pressed(option: String) -> void:
+ set_value(option)
+
+
+func _on_focus_entered() -> void:
+ indicator.remove_theme_color_override(&"font_color")
+ focused.emit()
+
+func _on_text_submitted(new_text: String) -> void:
+ indicator.release_focus()
+ if new_text in attribute.possible_values:
+ set_value(new_text)
+ elif new_text.is_empty():
+ set_value(attribute.default)
+ else:
+ sync(attribute.get_value())
+
+func _on_text_change_canceled() -> void:
+ sync(attribute.get_value())
+
+
+func _on_text_changed(new_text: String) -> void:
+ if new_text in attribute.possible_values:
+ indicator.add_theme_color_override(&"font_color", Color(0.6, 1.0, 0.6))
+ else:
+ indicator.add_theme_color_override(&"font_color", Color(1.0, 0.6, 0.6))
+
+func sync(new_value: String) -> void:
+ if indicator != null:
+ indicator.text = new_value
+ if new_value == attribute.default:
+ indicator.add_theme_color_override(&"font_color", Color(0.64, 0.64, 0.64))
+ else:
+ indicator.remove_theme_color_override(&"font_color")
+
+
+func _on_button_gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and\
+ event.is_pressed():
+ accept_event()
+ var mouse_motion_event := InputEventMouseMotion.new()
+ mouse_motion_event.position = get_viewport().get_mouse_position()
+ Input.parse_input_event(mouse_motion_event)
+ else:
+ button.mouse_filter = Utils.mouse_filter_pass_non_drag_events(event)
diff --git a/src/ui_elements/enum_field.tscn b/src/ui_elements/enum_field.tscn
new file mode 100644
index 000000000..148193ea4
--- /dev/null
+++ b/src/ui_elements/enum_field.tscn
@@ -0,0 +1,60 @@
+[gd_scene load_steps=6 format=3 uid="uid://d2da0thyq5rq8"]
+
+[ext_resource type="Script" path="res://src/ui_elements/enum_field.gd" id="1_1jqoy"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="2_4bajq"]
+[ext_resource type="Texture2D" uid="uid://coda6chhcatal" path="res://visual/icons/Arrow.svg" id="3_vhd8v"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_y4kmw"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qhmpn"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[node name="EnumField" type="HBoxContainer"]
+custom_minimum_size = Vector2(0, 22)
+offset_right = 73.0
+offset_bottom = 22.0
+theme_override_constants/separation = 0
+script = ExtResource("1_1jqoy")
+
+[node name="LineEdit" type="LineEdit" parent="."]
+custom_minimum_size = Vector2(58, 0)
+layout_mode = 2
+focus_mode = 1
+mouse_filter = 1
+theme_type_variation = &"RightConnectedLineEdit"
+script = ExtResource("2_4bajq")
+hover_stylebox = SubResource("StyleBoxFlat_y4kmw")
+focus_stylebox = SubResource("StyleBoxFlat_qhmpn")
+code_font_tooltip = true
+
+[node name="Button" type="Button" parent="."]
+custom_minimum_size = Vector2(15, 0)
+layout_mode = 2
+focus_mode = 0
+mouse_filter = 1
+mouse_default_cursor_shape = 2
+theme_type_variation = &"LeftConnectedButton"
+icon = ExtResource("3_vhd8v")
+expand_icon = true
+
+[connection signal="focus_entered" from="LineEdit" to="." method="_on_focus_entered"]
+[connection signal="text_change_canceled" from="LineEdit" to="." method="_on_text_change_canceled"]
+[connection signal="text_changed" from="LineEdit" to="." method="_on_text_changed"]
+[connection signal="text_submitted" from="LineEdit" to="." method="_on_text_submitted"]
+[connection signal="gui_input" from="Button" to="." method="_on_button_gui_input"]
+[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
diff --git a/src/ui_elements/flag_field.gd b/src/ui_elements/flag_field.gd
new file mode 100644
index 000000000..72442dd71
--- /dev/null
+++ b/src/ui_elements/flag_field.gd
@@ -0,0 +1,48 @@
+## An editor for flags with the value of 0 or 1, not tied to any attribute.
+extends Button
+
+var hovered := false
+
+signal value_changed(new_value: int)
+var _value: int
+
+func set_value(new_value: int, emit_value_changed := true) -> void:
+ if _value != new_value:
+ _value = new_value
+ if emit_value_changed:
+ value_changed.emit(new_value)
+
+func get_value() -> int:
+ return _value
+
+
+func _on_toggled(is_state_pressed: bool) -> void:
+ set_value(1 if is_state_pressed else 0)
+
+func _ready() -> void:
+ value_changed.connect(_on_value_changed)
+ button_pressed = (get_value() == 1)
+ text = String.num_uint64(get_value())
+
+func _on_value_changed(new_value: int) -> void:
+ button_pressed = (new_value == 1)
+ text = String.num_uint64(new_value)
+
+
+func _on_mouse_entered() -> void:
+ hovered = true
+ queue_redraw()
+
+func _on_mouse_exited() -> void:
+ hovered = false
+ queue_redraw()
+
+func _draw() -> void:
+ if hovered:
+ var hover_stylebox := StyleBoxFlat.new()
+ hover_stylebox.draw_center = false
+ hover_stylebox.corner_radius_top_left = 3
+ hover_stylebox.corner_radius_top_right = 3
+ hover_stylebox.border_width_bottom = 2
+ hover_stylebox.border_color = Color(1, 1, 1, 0.2)
+ draw_style_box(hover_stylebox, Rect2(Vector2.ZERO, size))
diff --git a/src/ui_elements/flag_field.tscn b/src/ui_elements/flag_field.tscn
new file mode 100644
index 000000000..a60bc2712
--- /dev/null
+++ b/src/ui_elements/flag_field.tscn
@@ -0,0 +1,53 @@
+[gd_scene load_steps=6 format=3 uid="uid://br8g7w38jguh4"]
+
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="1_p8s8y"]
+[ext_resource type="Script" path="res://src/ui_elements/flag_field.gd" id="2_0bhg4"]
+
+[sub_resource type="FontVariation" id="FontVariation_46ud6"]
+base_font = ExtResource("1_p8s8y")
+spacing_top = -1
+spacing_bottom = -1
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oo47u"]
+content_margin_left = 5.0
+content_margin_top = 0.0
+content_margin_right = 5.0
+content_margin_bottom = 0.0
+bg_color = Color(0.4, 0.121569, 0.121569, 1)
+border_width_bottom = 2
+border_color = Color(0.85098, 0.168627, 0.168627, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uepoa"]
+content_margin_left = 5.0
+content_margin_top = 0.0
+content_margin_right = 5.0
+content_margin_bottom = 0.0
+bg_color = Color(0.12, 0.4, 0.12, 1)
+border_width_bottom = 2
+border_color = Color(0.466667, 0.85098, 0.0862745, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+
+[node name="FlagField" type="Button"]
+custom_minimum_size = Vector2(0, 18)
+offset_right = 19.0
+offset_bottom = 18.0
+focus_mode = 0
+mouse_filter = 1
+mouse_default_cursor_shape = 2
+theme_override_colors/font_color = Color(1, 0.75, 0.75, 1)
+theme_override_colors/font_pressed_color = Color(0.74902, 1, 0.74902, 1)
+theme_override_fonts/font = SubResource("FontVariation_46ud6")
+theme_override_font_sizes/font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxFlat_oo47u")
+theme_override_styles/hover = SubResource("StyleBoxFlat_oo47u")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_uepoa")
+toggle_mode = true
+text = "0"
+script = ExtResource("2_0bhg4")
+
+[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"]
+[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
+[connection signal="toggled" from="." to="." method="_on_toggled"]
diff --git a/src/ui_elements/good_color_picker.gd b/src/ui_elements/good_color_picker.gd
new file mode 100644
index 000000000..a04fe9ebb
--- /dev/null
+++ b/src/ui_elements/good_color_picker.gd
@@ -0,0 +1,381 @@
+extends VBoxContainer
+
+const handle_texture = preload("res://visual/icons/handles/HandleBig.svg")
+const slider_arrow = preload("res://visual/icons/SliderArrow.svg")
+const side_slider_arrow = preload("res://visual/icons/SideSliderArrow.svg")
+const bg_pattern = preload("res://visual/icons/backgrounds/CheckerboardMini.svg")
+
+var UR := UndoRedo.new()
+
+enum SliderMode {RGB, HSV}
+var slider_mode: SliderMode:
+ set(new_mode):
+ slider_mode = new_mode
+ var disabled_button := hsv_button if new_mode == SliderMode.HSV else rgb_button
+ for btn in [hsv_button, rgb_button]:
+ btn.disabled = (btn == disabled_button)
+ btn.mouse_default_cursor_shape = Control.CURSOR_ARROW if\
+ btn == disabled_button else Control.CURSOR_POINTING_HAND
+ match slider_mode:
+ SliderMode.RGB:
+ tracks_arr[1].material.set_shader_parameter(&"interpolation", 0)
+ tracks_arr[2].material.set_shader_parameter(&"interpolation", 1)
+ tracks_arr[3].material.set_shader_parameter(&"interpolation", 2)
+ SliderMode.HSV:
+ tracks_arr[1].material.set_shader_parameter(&"interpolation", 3)
+ tracks_arr[2].material.set_shader_parameter(&"interpolation", 4)
+ tracks_arr[3].material.set_shader_parameter(&"interpolation", 5)
+ backup_display_color.h = clampf(backup_display_color.h, 0.0, 0.9999)
+ backup_display_color.v = clampf(backup_display_color.v, 0.0001, 1.0)
+ backup_display_color.s = clampf(backup_display_color.s, 0.0001, 1.0)
+ display_color.h = clampf(display_color.h, 0.0, 0.9999)
+ display_color.v = clampf(display_color.v, 0.0001, 1.0)
+ display_color.s = clampf(display_color.s, 0.0001, 1.0)
+ update()
+
+@onready var color_wheel: MarginContainer = $ShapeContainer/ColorWheel
+@onready var color_wheel_drawn: ColorRect = $ShapeContainer/ColorWheel/ColorWheelDraw
+@onready var rgb_button: Button = $SliderContainer/ColorSpaceContainer/RGB
+@onready var hsv_button: Button = $SliderContainer/ColorSpaceContainer/HSV
+@onready var start_color_rect: Control = %ColorsDisplay/StartColorRect
+@onready var color_rect: Control = %ColorsDisplay/ColorRect
+@onready var none_button: Button = $ColorContainer/NoneButton
+@onready var reset_color_button: Button = %ColorsDisplay/ColorRect/ResetColorButton
+@onready var center: Vector2 = color_wheel_drawn.get_rect().get_center()
+
+var color_wheel_surface := RenderingServer.canvas_item_create()
+
+# 0 is the side slider, 1-3 are the remaining sliders.
+var sliders_dragged: Array[bool] = [false, false, false, false]
+# Tracks are the color rects of the sliders.
+@onready var tracks_arr: Array[ColorRect] = [
+ $ShapeContainer/SideSlider/SideSliderTrack, %Slider1/MarginContainer/ColorTrack,
+ %Slider2/MarginContainer/ColorTrack, %Slider3/MarginContainer/ColorTrack]
+# Widgets are the margin containers that acts as click areas and draw the arrow.
+@onready var widgets_arr: Array[MarginContainer] = [
+ $ShapeContainer/SideSlider, %Slider1/MarginContainer, %Slider2/MarginContainer,
+ %Slider3/MarginContainer]
+# Fields are the number fields beside the color tracks.
+@onready var fields_arr: Array[BetterLineEdit] = [
+ null, %Slider1/IntField, %Slider2/IntField, %Slider3/IntField]
+
+# This variable stores what the color string was at the start, for the reset button.
+var starting_color: String
+var starting_display_color: Color
+# These store the old color when using "none" or starting a dragging operation.
+var backup_color: String
+var backup_display_color: Color
+# These store the current color string, normally forced to hex.
+var color: String
+var display_color: Color
+
+signal color_changed(new_color: String)
+
+
+func backup() -> void:
+ backup_color = color
+ backup_display_color = display_color
+
+# To be called right after the color picker is added.
+func setup_color(new_color: String) -> void:
+ starting_color = new_color
+ color = new_color
+ # Setup the display color.
+ starting_display_color = ColorParser.string_to_color(starting_color)
+ if slider_mode == SliderMode.HSV:
+ # Clamping like this doesn't change the hex representation, but
+ # it helps avoid locking certain sliders (e.g. hue slider when saturation is 0).
+ # The HVS order helps to keep the saturation at 0.0001 for some reason.
+ starting_display_color.h = clampf(starting_display_color.h, 0.0, 0.9999)
+ starting_display_color.v = clampf(starting_display_color.v, 0.0001, 1.0)
+ starting_display_color.s = clampf(starting_display_color.s, 0.0001, 1.0)
+ starting_display_color.a = 1
+ display_color = starting_display_color
+ slider_mode = GlobalSettings.save_data.color_picker_slider_mode
+ update()
+
+func _ready() -> void:
+ RenderingServer.canvas_item_set_parent(color_wheel_surface,
+ color_wheel_drawn.get_canvas_item())
+
+
+func register_visual_change(new_color: Color, use_backup := true) -> void:
+ UR.create_action("")
+ UR.add_do_method(set_color.bind(hex(new_color), new_color))
+ if use_backup:
+ UR.add_undo_method(set_color.bind(backup_color, backup_display_color))
+ else:
+ UR.add_undo_method(set_color.bind(color, display_color))
+ UR.commit_action()
+
+
+func set_color(new_color: String, new_display_color: Color) -> void:
+ set_display_color(new_display_color)
+ if color == new_color:
+ return
+
+ color = new_color
+ update()
+ color_changed.emit(new_color)
+
+
+func set_display_color(new_display_color: Color) -> void:
+ display_color = new_display_color
+ update()
+ color_changed.emit(hex(new_display_color))
+
+func update() -> void:
+ # Adjust the shaders.
+ tracks_arr[0].material.set_shader_parameter(&"v", display_color.v)
+ tracks_arr[0].material.set_shader_parameter(&"base_color",
+ Color.from_hsv(display_color.h, display_color.s, 1.0))
+ for i in [1, 2, 3]:
+ tracks_arr[i].material.set_shader_parameter(&"base_color", display_color)
+ # Setup the "none" button.
+ var is_none := (color == "none")
+ none_button.button_pressed = is_none
+ none_button.tooltip_text = tr(&"#enable_color") if is_none else tr(&"#disable_color")
+ # Redraw widgets, color indicators, color wheel.
+ color_rect.queue_redraw()
+ start_color_rect.queue_redraw()
+ queue_redraw()
+ color_wheel_drawn.queue_redraw()
+ for i in [0, 1, 2, 3]:
+ widgets_arr[i].queue_redraw()
+ # Set the text of the color fields.
+ match slider_mode:
+ SliderMode.RGB:
+ fields_arr[1].text = String.num_uint64(roundi(display_color.r * 255))
+ fields_arr[2].text = String.num_uint64(roundi(display_color.g * 255))
+ fields_arr[3].text = String.num_uint64(roundi(display_color.b * 255))
+ SliderMode.HSV:
+ fields_arr[1].text = String.num_uint64(roundi(display_color.h * 360))
+ fields_arr[2].text = String.num_uint64(roundi(display_color.s * 100))
+ fields_arr[3].text = String.num_uint64(roundi(display_color.v * 100))
+
+
+func _on_color_wheel_gui_input(event: InputEvent) -> void:
+ var is_event_drag_start := Utils.is_event_drag_start(event)
+ if is_event_drag_start:
+ backup()
+ var new_color := display_color
+ if Utils.is_event_drag(event) or is_event_drag_start:
+ var event_pos_on_wheel: Vector2 = event.position + color_wheel.position -\
+ color_wheel_drawn.position
+ new_color.h = fposmod(center.angle_to_point(event_pos_on_wheel), TAU) / TAU
+ new_color.s = minf(event_pos_on_wheel.distance_to(center) * 2 /\
+ color_wheel_drawn.size.x, 1.0)
+ set_display_color(new_color)
+ if Utils.is_event_drag_end(event):
+ register_visual_change(display_color)
+
+func start_slider_drag(idx: int) -> void:
+ sliders_dragged[idx] = true
+ backup()
+
+func move_slider(idx: int, offset: float) -> void:
+ var new_color := display_color
+ var channel: String
+ match idx:
+ 0: channel = "v"
+ 1: match slider_mode:
+ SliderMode.RGB: channel = "r"
+ SliderMode.HSV: channel = "h"
+ 2: match slider_mode:
+ SliderMode.RGB: channel = "g"
+ SliderMode.HSV: channel = "s"
+ 3: match slider_mode:
+ SliderMode.RGB: channel = "b"
+ SliderMode.HSV: channel = "v"
+ new_color = set_color_channel(new_color, channel, offset)
+ set_display_color(new_color)
+ widgets_arr[idx].queue_redraw()
+
+func set_color_channel(col: Color, channel: String, offset: float) -> Color:
+ match channel:
+ "r": col.r = clampf(offset, 0.0, 1.0)
+ "g": col.g = clampf(offset, 0.0, 1.0)
+ "b": col.b = clampf(offset, 0.0, 1.0)
+ "h": col.h = clampf(offset, 0.0, 0.9999)
+ "s": col.s = clampf(offset, 0.0001, 1.0)
+ "v": col.v = clampf(offset, 0.0001, 1.0)
+ return col
+
+func end_slider_drag(idx: int) -> void:
+ register_visual_change(display_color)
+ sliders_dragged[idx] = false
+ widgets_arr[idx].queue_redraw()
+
+func calculate_offset(idx: int, pos: Vector2, is_slider_vertical: bool) -> float:
+ if is_slider_vertical:
+ return 1 - ((pos.y - tracks_arr[idx].position.y) / tracks_arr[idx].size.y)
+ else:
+ return (pos.x - tracks_arr[idx].position.x) / tracks_arr[idx].size.x
+
+func parse_slider_input(event: InputEvent, idx: int, is_slider_vertical := false) -> void:
+ if Utils.is_event_drag_start(event):
+ start_slider_drag(idx)
+ move_slider(idx, calculate_offset(idx, event.position, is_slider_vertical))
+ elif Utils.is_event_drag(event):
+ move_slider(idx, calculate_offset(idx, event.position, is_slider_vertical))
+ elif Utils.is_event_drag_end(event):
+ end_slider_drag(idx)
+
+func _on_side_slider_gui_input(event: InputEvent) -> void:
+ parse_slider_input(event, 0, true)
+
+func _on_slider1_gui_input(event: InputEvent) -> void:
+ parse_slider_input(event, 1)
+
+func _on_slider2_gui_input(event: InputEvent) -> void:
+ parse_slider_input(event, 2)
+
+func _on_slider3_gui_input(event: InputEvent) -> void:
+ parse_slider_input(event, 3)
+
+func _on_slider1_text_submitted(new_text: String) -> void:
+ var new_color := display_color
+ match slider_mode:
+ SliderMode.RGB: new_color.r = clampf(new_text.to_int() / 255.0, 0.0, 1.0)
+ SliderMode.HSV: new_color.h = clampf(new_text.to_int() / 360.0, 0.0, 0.9999)
+ register_visual_change(new_color, false)
+
+func _on_slider2_text_submitted(new_text: String) -> void:
+ var new_color := display_color
+ match slider_mode:
+ SliderMode.RGB: new_color.g = clampf(new_text.to_int() / 255.0, 0.0, 1.0)
+ SliderMode.HSV: new_color.s = clampf(new_text.to_int() / 100.0, 0.0001, 1.0)
+ register_visual_change(new_color, false)
+
+func _on_slider3_text_submitted(new_text: String) -> void:
+ var new_color := display_color
+ match slider_mode:
+ SliderMode.RGB: new_color.b = clampf(new_text.to_int() / 255.0, 0.0, 1.0)
+ SliderMode.HSV: new_color.v = clampf(new_text.to_int() / 100.0, 0.0001, 1.0)
+ register_visual_change(new_color, false)
+
+func _on_none_button_pressed() -> void:
+ UR.create_action("")
+ if color.strip_edges() == "none":
+ UR.add_do_method(set_color.bind(backup_color, backup_display_color))
+ UR.add_undo_method(set_color.bind(color, display_color))
+ else:
+ backup()
+ UR.add_do_method(set_color.bind("none", display_color))
+ UR.add_undo_method(set_color.bind(color, display_color))
+ UR.commit_action()
+
+func _on_reset_color_button_pressed() -> void:
+ reset_color_button.disabled = true
+ UR.create_action("")
+ UR.add_do_method(set_color.bind(starting_color, starting_display_color))
+ UR.add_undo_method(set_color.bind(color, display_color))
+ UR.commit_action()
+
+
+func _on_rgb_pressed() -> void:
+ slider_mode = SliderMode.RGB
+ GlobalSettings.modify_save_data(&"color_picker_slider_mode", SliderMode.RGB)
+
+func _on_hsv_pressed() -> void:
+ slider_mode = SliderMode.HSV
+ GlobalSettings.modify_save_data(&"color_picker_slider_mode", SliderMode.HSV)
+
+
+# Gray out the start color rect if it's not actually a color.
+func _on_start_color_rect_draw() -> void:
+ var rect_size := start_color_rect.size
+ var rect := Rect2(Vector2.ZERO, rect_size)
+ if ColorParser.is_valid_url(https://github.com/starting_color):
+ var cross_color := Color(0.8, 0.8, 0.8)
+ start_color_rect.draw_rect(rect, Color(0.6, 0.6, 0.6))
+ start_color_rect.draw_line(Vector2.ZERO, rect_size, cross_color, 0.5, true)
+ start_color_rect.draw_line(Vector2(rect_size.x, 0), Vector2(0, rect_size.y),
+ cross_color, 0.5, true)
+ elif starting_color == "none":
+ start_color_rect.draw_texture_rect(bg_pattern, rect, true)
+ else:
+ start_color_rect.draw_rect(rect, starting_display_color)
+
+func _on_color_rect_draw() -> void:
+ var rect := Rect2(Vector2.ZERO, color_rect.size)
+ if color == "none":
+ color_rect.draw_texture_rect(bg_pattern, rect, true)
+ else:
+ color_rect.draw_rect(rect, display_color)
+
+# Draw inside the side slider to give it a little arrow to the side.
+func _on_side_slider_draw() -> void:
+ var arrow_modulate := Color(1, 1, 1) if sliders_dragged[0] else Color(1, 1, 1, 0.7)
+ widgets_arr[0].draw_texture(side_slider_arrow, Vector2(0, tracks_arr[0].position.y +\
+ tracks_arr[0].size.y * (1 - display_color.v) -\
+ side_slider_arrow.get_height() / 2.0), arrow_modulate)
+
+func _draw() -> void:
+ RenderingServer.canvas_item_clear(color_wheel_surface)
+ # Draw the color wheel handle.
+ var point_pos := center + Vector2(center.x * cos(display_color.h * TAU),
+ center.y * sin(display_color.h * TAU)) * display_color.s
+ RenderingServer.canvas_item_add_texture_rect(color_wheel_surface, Rect2(point_pos -\
+ handle_texture.get_size() / 2, handle_texture.get_size()), handle_texture)
+
+# Helper for drawing the horizontal sliders.
+func draw_hslider(idx: int, offset: float, chr: String) -> void:
+ var arrow_modulate := Color(1, 1, 1) if sliders_dragged[idx] else Color(1, 1, 1, 0.7)
+ widgets_arr[idx].draw_texture(slider_arrow, Vector2(tracks_arr[idx].position.x +\
+ tracks_arr[idx].size.x * offset - slider_arrow.get_width() / 2.0,
+ tracks_arr[idx].size.y), arrow_modulate)
+ widgets_arr[idx].draw_string(ThemeDB.get_project_theme().default_font,
+ Vector2(-12, 11), chr, HORIZONTAL_ALIGNMENT_CENTER, 12, 14)
+
+func _on_slider1_draw() -> void:
+ match slider_mode:
+ SliderMode.RGB: draw_hslider(1, display_color.r, "R")
+ SliderMode.HSV: draw_hslider(1, display_color.h, "H")
+
+func _on_slider2_draw() -> void:
+ match slider_mode:
+ SliderMode.RGB: draw_hslider(2, display_color.g, "G")
+ SliderMode.HSV: draw_hslider(2, display_color.s, "S")
+
+func _on_slider3_draw() -> void:
+ match slider_mode:
+ SliderMode.RGB: draw_hslider(3, display_color.b, "B")
+ SliderMode.HSV: draw_hslider(3, display_color.v, "V")
+
+
+func _on_reset_color_button_gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion and event.button_mask != MOUSE_BUTTON_MASK_LEFT:
+ if ColorParser.are_colors_same(starting_color, color):
+ reset_color_button.disabled = true
+ return
+ reset_color_button.disabled = false
+ if display_color.get_luminance() < 0.455:
+ reset_color_button.begin_bulk_theme_override()
+ reset_color_button.add_theme_color_override(&"icon_hover_color", Color.WHITE)
+ reset_color_button.add_theme_color_override(&"icon_pressed_color",
+ Color(0.5, 1, 1))
+ reset_color_button.end_bulk_theme_override()
+ else:
+ reset_color_button.begin_bulk_theme_override()
+ reset_color_button.add_theme_color_override(&"icon_hover_color", Color.BLACK)
+ reset_color_button.add_theme_color_override(&"icon_pressed_color",
+ Color(0, 0.5, 0.5))
+ reset_color_button.end_bulk_theme_override()
+
+func hex(col: Color) -> String:
+ return col.to_html(false)
+
+
+func _input(event: InputEvent) -> void:
+ if not visible:
+ return
+
+ if event.is_action_pressed(&"redo"):
+ if UR.has_redo():
+ UR.redo()
+ accept_event()
+ elif event.is_action_pressed(&"undo"):
+ if UR.has_undo():
+ UR.undo()
+ accept_event()
diff --git a/src/ui_elements/good_color_picker.tscn b/src/ui_elements/good_color_picker.tscn
new file mode 100644
index 000000000..cd601f1a9
--- /dev/null
+++ b/src/ui_elements/good_color_picker.tscn
@@ -0,0 +1,291 @@
+[gd_scene load_steps=16 format=3 uid="uid://b1eig44cov474"]
+
+[ext_resource type="Script" path="res://src/ui_elements/good_color_picker.gd" id="1_0pc78"]
+[ext_resource type="Shader" path="res://src/shaders/color_wheel.gdshader" id="2_nf1uk"]
+[ext_resource type="Shader" path="res://src/shaders/slider_visuals.gdshader" id="5_acxpg"]
+[ext_resource type="Texture2D" uid="uid://cvh3kwbucf2n1" path="res://visual/icons/Reload.svg" id="5_rh0xc"]
+[ext_resource type="Texture2D" uid="uid://d36qn2f7a0nok" path="res://visual/icons/NoneColor.svg" id="5_rnr60"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="6_aqyoh"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_wl372"]
+shader = ExtResource("2_nf1uk")
+shader_parameter/v = 1.0
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_gihhg"]
+shader = ExtResource("5_acxpg")
+shader_parameter/base_color = Vector3(0, 0, 0)
+shader_parameter/interpolation = 5
+shader_parameter/horizontal = false
+shader_parameter/inverted = true
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mqx76"]
+content_margin_top = 1.0
+content_margin_bottom = 1.0
+bg_color = Color(0.866667, 0.933333, 1, 0.133333)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cle8x"]
+content_margin_top = 1.0
+content_margin_bottom = 1.0
+bg_color = Color(0.866667, 0.933333, 1, 0.2)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sndty"]
+bg_color = Color(0.866667, 0.933333, 1, 0.333333)
+border_width_top = 2
+
+[sub_resource type="ButtonGroup" id="ButtonGroup_w0iiw"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_fvg6q"]
+shader = ExtResource("5_acxpg")
+shader_parameter/base_color = Vector3(1, 1, 1)
+shader_parameter/interpolation = 0
+shader_parameter/horizontal = true
+shader_parameter/inverted = false
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_dlmj5"]
+shader = ExtResource("5_acxpg")
+shader_parameter/base_color = Vector3(1, 1, 1)
+shader_parameter/interpolation = 0
+shader_parameter/horizontal = true
+shader_parameter/inverted = false
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_p3pbp"]
+shader = ExtResource("5_acxpg")
+shader_parameter/base_color = Vector3(1, 1, 1)
+shader_parameter/interpolation = 0
+shader_parameter/horizontal = true
+shader_parameter/inverted = false
+
+[node name="GoodColorPicker" type="VBoxContainer"]
+offset_right = 190.0
+offset_bottom = 266.0
+theme_override_constants/separation = 6
+script = ExtResource("1_0pc78")
+
+[node name="ShapeContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 0
+theme_override_constants/separation = 0
+alignment = 1
+
+[node name="ColorWheel" type="MarginContainer" parent="ShapeContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 6
+theme_override_constants/margin_top = 6
+theme_override_constants/margin_right = 6
+theme_override_constants/margin_bottom = 6
+
+[node name="ColorWheelDraw" type="ColorRect" parent="ShapeContainer/ColorWheel"]
+material = SubResource("ShaderMaterial_wl372")
+custom_minimum_size = Vector2(160, 160)
+layout_mode = 2
+mouse_filter = 1
+
+[node name="SideSlider" type="MarginContainer" parent="ShapeContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 4
+theme_override_constants/margin_bottom = 4
+
+[node name="SideSliderTrack" type="ColorRect" parent="ShapeContainer/SideSlider"]
+material = SubResource("ShaderMaterial_gihhg")
+custom_minimum_size = Vector2(18, 0)
+layout_mode = 2
+mouse_filter = 1
+
+[node name="ColorContainer" type="HBoxContainer" parent="."]
+custom_minimum_size = Vector2(0, 16)
+layout_mode = 2
+theme_override_constants/separation = 6
+alignment = 1
+
+[node name="NoneButton" type="Button" parent="ColorContainer"]
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+theme_override_font_sizes/font_size = 10
+toggle_mode = true
+icon = ExtResource("5_rnr60")
+
+[node name="ColorsDisplay" type="HBoxContainer" parent="ColorContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(160, 16)
+layout_mode = 2
+size_flags_vertical = 4
+theme_override_constants/separation = 0
+
+[node name="StartColorRect" type="Control" parent="ColorContainer/ColorsDisplay"]
+clip_contents = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="ColorRect" type="Control" parent="ColorContainer/ColorsDisplay"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="ResetColorButton" type="Button" parent="ColorContainer/ColorsDisplay/ColorRect"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+focus_mode = 0
+theme_type_variation = &"TextButton"
+theme_override_colors/icon_normal_color = Color(0, 0, 0, 0)
+theme_override_colors/icon_disabled_color = Color(0, 0, 0, 0)
+icon = ExtResource("5_rh0xc")
+icon_alignment = 1
+
+[node name="SliderContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="ColorSpaceContainer" type="HBoxContainer" parent="SliderContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="RGB" type="Button" parent="SliderContainer/ColorSpaceContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_override_colors/font_disabled_color = Color(0.866667, 0.933333, 1, 1)
+theme_override_styles/normal = SubResource("StyleBoxFlat_mqx76")
+theme_override_styles/hover = SubResource("StyleBoxFlat_cle8x")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_sndty")
+theme_override_styles/disabled = SubResource("StyleBoxFlat_sndty")
+toggle_mode = true
+button_group = SubResource("ButtonGroup_w0iiw")
+text = "RGB"
+
+[node name="HSV" type="Button" parent="SliderContainer/ColorSpaceContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_override_colors/font_disabled_color = Color(0.866667, 0.933333, 1, 1)
+theme_override_styles/normal = SubResource("StyleBoxFlat_mqx76")
+theme_override_styles/hover = SubResource("StyleBoxFlat_cle8x")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_sndty")
+theme_override_styles/disabled = SubResource("StyleBoxFlat_sndty")
+toggle_mode = true
+button_group = SubResource("ButtonGroup_w0iiw")
+text = "HSV"
+
+[node name="HBoxContainer" type="HBoxContainer" parent="SliderContainer"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="Spacer" type="Control" parent="SliderContainer/HBoxContainer"]
+custom_minimum_size = Vector2(10, 0)
+layout_mode = 2
+
+[node name="TracksContainer" type="VBoxContainer" parent="SliderContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 6
+alignment = 2
+
+[node name="Slider1" type="HBoxContainer" parent="SliderContainer/HBoxContainer/TracksContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 4
+
+[node name="MarginContainer" type="MarginContainer" parent="SliderContainer/HBoxContainer/TracksContainer/Slider1"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 6
+
+[node name="ColorTrack" type="ColorRect" parent="SliderContainer/HBoxContainer/TracksContainer/Slider1/MarginContainer"]
+material = SubResource("ShaderMaterial_fvg6q")
+custom_minimum_size = Vector2(0, 13)
+layout_mode = 2
+size_flags_horizontal = 3
+mouse_filter = 1
+
+[node name="IntField" type="LineEdit" parent="SliderContainer/HBoxContainer/TracksContainer/Slider1"]
+layout_mode = 2
+size_flags_vertical = 0
+theme_type_variation = &"GoodColorPickerLineEdit"
+alignment = 1
+max_length = 3
+script = ExtResource("6_aqyoh")
+
+[node name="Slider2" type="HBoxContainer" parent="SliderContainer/HBoxContainer/TracksContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 4
+
+[node name="MarginContainer" type="MarginContainer" parent="SliderContainer/HBoxContainer/TracksContainer/Slider2"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 6
+
+[node name="ColorTrack" type="ColorRect" parent="SliderContainer/HBoxContainer/TracksContainer/Slider2/MarginContainer"]
+material = SubResource("ShaderMaterial_dlmj5")
+custom_minimum_size = Vector2(0, 13)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+mouse_filter = 1
+
+[node name="IntField" type="LineEdit" parent="SliderContainer/HBoxContainer/TracksContainer/Slider2"]
+layout_mode = 2
+size_flags_vertical = 0
+theme_type_variation = &"GoodColorPickerLineEdit"
+alignment = 1
+max_length = 3
+script = ExtResource("6_aqyoh")
+
+[node name="Slider3" type="HBoxContainer" parent="SliderContainer/HBoxContainer/TracksContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 4
+
+[node name="MarginContainer" type="MarginContainer" parent="SliderContainer/HBoxContainer/TracksContainer/Slider3"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 6
+
+[node name="ColorTrack" type="ColorRect" parent="SliderContainer/HBoxContainer/TracksContainer/Slider3/MarginContainer"]
+material = SubResource("ShaderMaterial_p3pbp")
+custom_minimum_size = Vector2(0, 13)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+mouse_filter = 1
+
+[node name="IntField" type="LineEdit" parent="SliderContainer/HBoxContainer/TracksContainer/Slider3"]
+layout_mode = 2
+size_flags_vertical = 0
+theme_type_variation = &"GoodColorPickerLineEdit"
+alignment = 1
+max_length = 3
+script = ExtResource("6_aqyoh")
+
+[connection signal="gui_input" from="ShapeContainer/ColorWheel" to="." method="_on_color_wheel_gui_input"]
+[connection signal="draw" from="ShapeContainer/SideSlider" to="." method="_on_side_slider_draw"]
+[connection signal="gui_input" from="ShapeContainer/SideSlider" to="." method="_on_side_slider_gui_input"]
+[connection signal="pressed" from="ColorContainer/NoneButton" to="." method="_on_none_button_pressed"]
+[connection signal="draw" from="ColorContainer/ColorsDisplay/StartColorRect" to="." method="_on_start_color_rect_draw"]
+[connection signal="draw" from="ColorContainer/ColorsDisplay/ColorRect" to="." method="_on_color_rect_draw"]
+[connection signal="gui_input" from="ColorContainer/ColorsDisplay/ColorRect/ResetColorButton" to="." method="_on_reset_color_button_gui_input"]
+[connection signal="pressed" from="ColorContainer/ColorsDisplay/ColorRect/ResetColorButton" to="." method="_on_reset_color_button_pressed"]
+[connection signal="pressed" from="SliderContainer/ColorSpaceContainer/RGB" to="." method="_on_rgb_pressed"]
+[connection signal="pressed" from="SliderContainer/ColorSpaceContainer/HSV" to="." method="_on_hsv_pressed"]
+[connection signal="draw" from="SliderContainer/HBoxContainer/TracksContainer/Slider1/MarginContainer" to="." method="_on_slider1_draw"]
+[connection signal="gui_input" from="SliderContainer/HBoxContainer/TracksContainer/Slider1/MarginContainer" to="." method="_on_slider1_gui_input"]
+[connection signal="text_submitted" from="SliderContainer/HBoxContainer/TracksContainer/Slider1/IntField" to="." method="_on_slider1_text_submitted"]
+[connection signal="draw" from="SliderContainer/HBoxContainer/TracksContainer/Slider2/MarginContainer" to="." method="_on_slider2_draw"]
+[connection signal="gui_input" from="SliderContainer/HBoxContainer/TracksContainer/Slider2/MarginContainer" to="." method="_on_slider2_gui_input"]
+[connection signal="text_submitted" from="SliderContainer/HBoxContainer/TracksContainer/Slider2/IntField" to="." method="_on_slider2_text_submitted"]
+[connection signal="draw" from="SliderContainer/HBoxContainer/TracksContainer/Slider3/MarginContainer" to="." method="_on_slider3_draw"]
+[connection signal="gui_input" from="SliderContainer/HBoxContainer/TracksContainer/Slider3/MarginContainer" to="." method="_on_slider3_gui_input"]
+[connection signal="text_submitted" from="SliderContainer/HBoxContainer/TracksContainer/Slider3/IntField" to="." method="_on_slider3_text_submitted"]
diff --git a/src/ui_elements/mini_number_field.gd b/src/ui_elements/mini_number_field.gd
new file mode 100644
index 000000000..e4d8050d1
--- /dev/null
+++ b/src/ui_elements/mini_number_field.gd
@@ -0,0 +1,33 @@
+## A minimalistic numeric editor, not tied to an attribute.
+extends BetterLineEdit
+
+enum Mode {DEFAULT, ONLY_POSITIVE, ANGLE, HALF_ANGLE}
+var mode := Mode.DEFAULT
+
+signal value_changed(new_value: float)
+var _value := NAN # Must not be updated directly.
+
+func set_value(new_value: float, no_signal := false) -> void:
+ if not is_finite(new_value):
+ text = NumberArrayParser.basic_num_to_text(_value)
+ return
+ text = NumberArrayParser.basic_num_to_text(new_value)
+ if new_value != _value:
+ _value = new_value
+ if not no_signal:
+ value_changed.emit(new_value)
+
+func get_value() -> float:
+ return _value
+
+
+func _on_text_submitted(submitted_text: String) -> void:
+ set_value(evaluate_after_input(submitted_text))
+
+func evaluate_after_input(eval_text: String) -> float:
+ var num := AttributeNumeric.evaluate_expr(eval_text)
+ match mode:
+ Mode.ONLY_POSITIVE: return maxf(num, 0.0001)
+ Mode.HALF_ANGLE: return fmod(num, 180.0)
+ Mode.ANGLE: return fmod(num, 360.0)
+ _: return num
diff --git a/src/ui_elements/mini_number_field.tscn b/src/ui_elements/mini_number_field.tscn
new file mode 100644
index 000000000..8e44c4212
--- /dev/null
+++ b/src/ui_elements/mini_number_field.tscn
@@ -0,0 +1,28 @@
+[gd_scene load_steps=4 format=3 uid="uid://cm3uphorskcix"]
+
+[ext_resource type="Script" path="res://src/ui_elements/mini_number_field.gd" id="1_ggi5v"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wk41m"]
+draw_center = false
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.133333)
+corner_detail = 1
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ul27o"]
+draw_center = false
+border_width_bottom = 2
+border_color = Color(0.501961, 1, 1, 0.4)
+corner_detail = 1
+
+[node name="MiniNumberField" type="LineEdit"]
+custom_minimum_size = Vector2(0, 18)
+offset_right = 44.0
+offset_bottom = 14.0
+mouse_filter = 1
+theme_type_variation = &"MiniLineEdit"
+script = ExtResource("1_ggi5v")
+hover_stylebox = SubResource("StyleBoxFlat_wk41m")
+focus_stylebox = SubResource("StyleBoxFlat_ul27o")
+code_font_tooltip = true
+
+[connection signal="text_submitted" from="." to="." method="_on_text_submitted"]
diff --git a/src/ui_elements/number_edit.gd b/src/ui_elements/number_edit.gd
new file mode 100644
index 000000000..7c3e44ff7
--- /dev/null
+++ b/src/ui_elements/number_edit.gd
@@ -0,0 +1,44 @@
+## A number editor, not tied to any attribute.
+extends BetterLineEdit
+
+@export var min_value := 0.0
+@export var max_value := 1.0
+@export var initial_value := 0.5
+@export var allow_lower := true
+@export var allow_higher := true
+@export var is_float := true
+
+signal value_changed(new_value: float)
+var _value := NAN
+
+func set_value(new_value: float, emit_changed := true) -> void:
+ if not is_finite(new_value):
+ sync_text()
+ return
+ elif _value != new_value:
+ if not allow_higher and new_value > max_value:
+ new_value = max_value
+ elif not allow_lower and new_value < min_value:
+ new_value = min_value
+ if _value != new_value:
+ _value = new_value
+ if emit_changed:
+ value_changed.emit(_value)
+ sync_text()
+
+func get_value() -> float:
+ return _value
+
+
+func _ready() -> void:
+ super()
+ # Done like this so a signal isn't emitted.
+ _value = initial_value
+ text = String.num(_value, 4)
+
+
+func _on_text_submitted(submitted_text: String) -> void:
+ set_value(AttributeNumeric.evaluate_expr(submitted_text))
+
+func sync_text() -> void:
+ text = String.num(_value, 4)
diff --git a/src/ui_elements/number_edit.tscn b/src/ui_elements/number_edit.tscn
new file mode 100644
index 000000000..917a781f3
--- /dev/null
+++ b/src/ui_elements/number_edit.tscn
@@ -0,0 +1,38 @@
+[gd_scene load_steps=4 format=3 uid="uid://dad7fkhmsooc6"]
+
+[ext_resource type="Script" path="res://src/ui_elements/number_edit.gd" id="1_dywrg"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oa7o2"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ucydw"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="NumberEdit" type="LineEdit"]
+custom_minimum_size = Vector2(0, 22)
+offset_right = 35.8125
+offset_bottom = 21.0
+focus_mode = 1
+script = ExtResource("1_dywrg")
+hover_stylebox = SubResource("StyleBoxFlat_oa7o2")
+focus_stylebox = SubResource("StyleBoxFlat_ucydw")
+
+[connection signal="text_submitted" from="." to="." method="_on_text_submitted"]
diff --git a/src/ui_elements/number_field.gd b/src/ui_elements/number_field.gd
new file mode 100644
index 000000000..3657909bc
--- /dev/null
+++ b/src/ui_elements/number_field.gd
@@ -0,0 +1,71 @@
+## An editor to be tied to a numeric attribute.
+extends BetterLineEdit
+
+signal focused
+var attribute: AttributeNumeric
+var attribute_name: String
+
+var min_value := 0.0
+var max_value := 1.0
+var allow_lower := true
+var allow_higher := true
+
+func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+ var numeric_value := AttributeNumeric.evaluate_expr(new_value)
+ # Validate the value.
+ if !is_finite(numeric_value):
+ sync(attribute.get_value())
+ return
+
+ if not allow_higher and numeric_value > max_value:
+ numeric_value = max_value
+ new_value = NumberParser.num_to_text(numeric_value)
+ elif not allow_lower and numeric_value < min_value:
+ numeric_value = min_value
+ new_value = NumberParser.num_to_text(numeric_value)
+
+ # Just because the value passed was +1 or 1.0 instead of the default 1,
+ # shouldn't cause the attribute to be added to the SVG text.
+ if NumberParser.text_to_num(attribute.default) == numeric_value:
+ new_value = attribute.default
+ elif NumberParser.text_to_num(new_value) != AttributeNumeric.evaluate_expr(new_value):
+ new_value = NumberParser.num_to_text(numeric_value)
+
+ sync(attribute.autoformat(new_value))
+ # Update the attribute.
+ if new_value != attribute.get_value() or update_type == Utils.UpdateType.FINAL:
+ match update_type:
+ Utils.UpdateType.INTERMEDIATE:
+ attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
+ Utils.UpdateType.FINAL:
+ attribute.set_value(new_value, Attribute.SyncMode.FINAL)
+ _:
+ attribute.set_value(new_value)
+
+
+func _ready() -> void:
+ super()
+ set_value(attribute.get_value())
+ attribute.value_changed.connect(set_value)
+ tooltip_text = attribute_name
+
+func _on_focus_entered() -> void:
+ remove_theme_color_override(&"font_color")
+ focused.emit()
+ super()
+
+func _on_text_submitted(submitted_text: String) -> void:
+ if submitted_text.strip_edges().is_empty():
+ set_value(attribute.default)
+ else:
+ set_value(submitted_text)
+
+func _on_text_change_canceled() -> void:
+ sync(attribute.get_value())
+
+func sync(new_value: String) -> void:
+ text = new_value
+ if new_value == attribute.default:
+ add_theme_color_override(&"font_color", Color(0.64, 0.64, 0.64))
+ else:
+ remove_theme_color_override(&"font_color")
diff --git a/src/ui_elements/number_field.tscn b/src/ui_elements/number_field.tscn
new file mode 100644
index 000000000..ff8a37354
--- /dev/null
+++ b/src/ui_elements/number_field.tscn
@@ -0,0 +1,42 @@
+[gd_scene load_steps=4 format=3 uid="uid://c6vgjud6wrdu4"]
+
+[ext_resource type="Script" path="res://src/ui_elements/number_field.gd" id="1_saak1"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nuwbn"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_veyl6"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="NumberField" type="LineEdit"]
+custom_minimum_size = Vector2(54, 22)
+offset_right = 35.8125
+offset_bottom = 21.0
+size_flags_horizontal = 0
+size_flags_vertical = 0
+focus_mode = 1
+script = ExtResource("1_saak1")
+hover_stylebox = SubResource("StyleBoxFlat_nuwbn")
+focus_stylebox = SubResource("StyleBoxFlat_veyl6")
+code_font_tooltip = true
+
+[connection signal="text_change_canceled" from="." to="." method="_on_text_change_canceled"]
+[connection signal="text_submitted" from="." to="." method="_on_text_submitted"]
diff --git a/src/ui_elements/number_field_with_slider.gd b/src/ui_elements/number_field_with_slider.gd
new file mode 100644
index 000000000..3008ab051
--- /dev/null
+++ b/src/ui_elements/number_field_with_slider.gd
@@ -0,0 +1,163 @@
+## An editor to be tied to a numeric attribute, plus a slider widget.
+extends HBoxContainer
+
+signal focused
+var attribute: AttributeNumeric
+var attribute_name: String
+
+@onready var num_edit: LineEdit = $LineEdit
+@onready var slider: Button = $Slider
+
+var slider_step := 0.01
+var min_value := 0.0
+var max_value := 1.0
+var allow_lower := true
+var allow_higher := true
+
+func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+ var numeric_value := AttributeNumeric.evaluate_expr(new_value)
+ # Validate the value.
+ if !is_finite(numeric_value):
+ sync(attribute.get_value())
+ return
+
+ if not allow_higher and numeric_value > max_value:
+ numeric_value = max_value
+ new_value = NumberParser.num_to_text(numeric_value)
+ elif not allow_lower and numeric_value < min_value:
+ numeric_value = min_value
+ new_value = NumberParser.num_to_text(numeric_value)
+
+ # Just because the value passed was +1 or 1.0 instead of the default 1,
+ # shouldn't cause the attribute to be added to the SVG text.
+ if attribute.default == NumberParser.num_to_text(numeric_value):
+ new_value = attribute.default
+ elif NumberParser.text_to_num(new_value) != AttributeNumeric.evaluate_expr(new_value):
+ new_value = NumberParser.num_to_text(numeric_value)
+
+ sync(attribute.autoformat(new_value))
+ # Update the attribute.
+ if new_value != attribute.get_value() or update_type == Utils.UpdateType.FINAL:
+ match update_type:
+ Utils.UpdateType.INTERMEDIATE:
+ attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
+ Utils.UpdateType.FINAL:
+ attribute.set_value(new_value, Attribute.SyncMode.FINAL)
+ _:
+ attribute.set_value(new_value)
+
+func set_num(new_number: float, update_type := Utils.UpdateType.REGULAR) -> void:
+ set_value(NumberParser.num_to_text(new_number), update_type)
+
+
+func _ready() -> void:
+ set_value(attribute.get_value())
+ attribute.value_changed.connect(set_value)
+ num_edit.tooltip_text = attribute_name
+
+func _on_focus_entered() -> void:
+ num_edit.remove_theme_color_override(&"font_color")
+ focused.emit()
+
+func _on_text_submitted(submitted_text: String) -> void:
+ if submitted_text.strip_edges().is_empty():
+ set_value(attribute.default)
+ else:
+ set_value(submitted_text)
+
+func _on_text_change_canceled() -> void:
+ sync(attribute.get_value())
+
+func sync(new_value: String) -> void:
+ if num_edit != null:
+ num_edit.text = new_value
+ if new_value == attribute.default:
+ num_edit.add_theme_color_override(&"font_color", Color(0.64, 0.64, 0.64))
+ else:
+ num_edit.remove_theme_color_override(&"font_color")
+ queue_redraw()
+
+
+# Slider
+
+var initial_slider_value: float
+var slider_dragged := false:
+ set(new_value):
+ if slider_dragged != new_value:
+ slider_dragged = new_value
+ queue_redraw()
+ if not slider_hovered:
+ get_viewport().update_mouse_cursor_state()
+ # FIXME workaround because "button_pressed" remains true
+ # if you unclick while outside of the area, for some reason.
+ # Couldn't replicate this in a minimal project.
+ remove_child(slider)
+ add_child(slider)
+
+var slider_hovered := false:
+ set(new_value):
+ if slider_hovered != new_value:
+ slider_hovered = new_value
+ queue_redraw()
+
+func _draw() -> void:
+ var slider_size := slider.get_size()
+ var line_edit_size := num_edit.get_size()
+ draw_set_transform(Vector2(line_edit_size.x, 1))
+ var stylebox := StyleBoxFlat.new()
+ stylebox.corner_radius_top_right = 5
+ stylebox.corner_radius_bottom_right = 5
+ stylebox.bg_color = num_edit.get_theme_stylebox(&"normal", &"LineEdit").bg_color
+ draw_style_box(stylebox, Rect2(Vector2.ZERO, slider_size - Vector2(1, 2)))
+ var fill_height := (slider_size.y - 4) * (attribute.get_num() - min_value) / max_value
+ if slider_dragged:
+ draw_rect(Rect2(0, 1 + slider_size.y - 4 - fill_height,
+ slider_size.x - 2, fill_height), Color("#def"))
+ elif slider_hovered:
+ draw_rect(Rect2(0, 1 + slider_size.y - 4 - fill_height,
+ slider_size.x - 2, fill_height), Color("#defb"))
+ else:
+ draw_rect(Rect2(0, 1 + slider_size.y - 4 - fill_height,
+ slider_size.x - 2, fill_height), Color("#def8"))
+
+func _on_slider_resized() -> void:
+ queue_redraw() # Whyyyyy are their sizes wrong at first...
+
+func _on_slider_gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and\
+ event.is_pressed():
+ accept_event()
+ var mouse_motion_event := InputEventMouseMotion.new()
+ mouse_motion_event.position = get_viewport().get_mouse_position()
+ Input.parse_input_event(mouse_motion_event)
+ else:
+ slider.mouse_filter = Utils.mouse_filter_pass_non_drag_events(event)
+
+ if not slider_dragged:
+ if event is InputEventMouseMotion and event.button_mask == 0:
+ slider_hovered = true
+ if Utils.is_event_drag_start(event):
+ slider_dragged = true
+ initial_slider_value = attribute.get_num()
+ set_num(get_slider_value_at_y(event.position.y), Utils.UpdateType.INTERMEDIATE)
+ else:
+ if Utils.is_event_drag(event):
+ set_num(get_slider_value_at_y(event.position.y), Utils.UpdateType.INTERMEDIATE)
+ elif Utils.is_event_drag_end(event):
+ slider_dragged = false
+ var final_slider_value := get_slider_value_at_y(event.position.y)
+ if initial_slider_value != final_slider_value:
+ set_num(final_slider_value, Utils.UpdateType.FINAL)
+
+func _unhandled_input(event: InputEvent) -> void:
+ if slider_dragged and Utils.is_event_cancel(event):
+ slider_dragged = false
+ set_num(initial_slider_value, Utils.UpdateType.INTERMEDIATE)
+ accept_event()
+
+func get_slider_value_at_y(y_coord: float) -> float:
+ return snappedf(lerpf(max_value, min_value,
+ (y_coord - 4) / (slider.get_size().y - 4)), slider_step)
+
+func _on_slider_mouse_exited() -> void:
+ slider_hovered = false
diff --git a/src/ui_elements/number_field_with_slider.tscn b/src/ui_elements/number_field_with_slider.tscn
new file mode 100644
index 000000000..3ef4cf818
--- /dev/null
+++ b/src/ui_elements/number_field_with_slider.tscn
@@ -0,0 +1,59 @@
+[gd_scene load_steps=5 format=3 uid="uid://bp2vpf7g8w8aj"]
+
+[ext_resource type="Script" path="res://src/ui_elements/number_field_with_slider.gd" id="1_ymm02"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="2_ytia1"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e034s"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0nb8u"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[node name="NumberFieldWithSlider" type="HBoxContainer"]
+custom_minimum_size = Vector2(0, 22)
+offset_right = 46.0
+offset_bottom = 22.0
+theme_override_constants/separation = 0
+script = ExtResource("1_ymm02")
+
+[node name="LineEdit" type="LineEdit" parent="."]
+custom_minimum_size = Vector2(46, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+focus_mode = 1
+mouse_filter = 1
+theme_type_variation = &"RightConnectedLineEdit"
+script = ExtResource("2_ytia1")
+hover_stylebox = SubResource("StyleBoxFlat_e034s")
+focus_stylebox = SubResource("StyleBoxFlat_0nb8u")
+code_font_tooltip = true
+
+[node name="Slider" type="Button" parent="."]
+clip_contents = true
+custom_minimum_size = Vector2(12, 0)
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"LeftConnectedButtonTransparent"
+keep_pressed_outside = true
+
+[connection signal="focus_entered" from="LineEdit" to="." method="_on_focus_entered"]
+[connection signal="text_change_canceled" from="LineEdit" to="." method="_on_text_change_canceled"]
+[connection signal="text_submitted" from="LineEdit" to="." method="_on_text_submitted"]
+[connection signal="gui_input" from="Slider" to="." method="_on_slider_gui_input"]
+[connection signal="mouse_exited" from="Slider" to="." method="_on_slider_mouse_exited"]
+[connection signal="resized" from="Slider" to="." method="_on_slider_resized"]
diff --git a/src/ui_elements/path_command_button.gd b/src/ui_elements/path_command_button.gd
new file mode 100644
index 000000000..e9c11d7ca
--- /dev/null
+++ b/src/ui_elements/path_command_button.gd
@@ -0,0 +1,34 @@
+## A button for a path command picker.
+extends Button
+
+signal pressed_custom(cmd_char: String)
+
+@onready var rtl: RichTextLabel = $RichTextLabel
+
+@export var command_char := ""
+
+func _ready() -> void:
+ text = ""
+ update_text()
+ pressed.connect(emit_pressed_custom)
+
+func emit_pressed_custom() -> void:
+ pressed_custom.emit(command_char)
+
+func update_text() -> void:
+ rtl.text = ""
+ rtl.clear()
+ rtl.push_bold()
+ rtl.add_text(command_char)
+ rtl.add_text(":")
+ rtl.pop()
+ rtl.add_text(" ")
+ rtl.add_text(Utils.path_command_char_dict[command_char.to_upper()])
+
+func set_invalid(new_state := true) -> void:
+ if new_state:
+ rtl.add_theme_color_override(&"default_color", Color(0.5, 0.5, 0.5))
+ else:
+ rtl.remove_theme_color_override(&"default_color")
+ disabled = new_state
+ mouse_default_cursor_shape = CURSOR_ARROW if new_state else CURSOR_POINTING_HAND
diff --git a/src/ui_elements/path_command_button.tscn b/src/ui_elements/path_command_button.tscn
new file mode 100644
index 000000000..5b50b94f8
--- /dev/null
+++ b/src/ui_elements/path_command_button.tscn
@@ -0,0 +1,27 @@
+[gd_scene load_steps=2 format=3 uid="uid://co2btefrqrm0e"]
+
+[ext_resource type="Script" path="res://src/ui_elements/path_command_button.gd" id="1_q6blt"]
+
+[node name="PathCommandButton" type="Button"]
+custom_minimum_size = Vector2(0, 25)
+anchors_preset = 10
+anchor_right = 1.0
+grow_horizontal = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+mouse_default_cursor_shape = 2
+theme_type_variation = &"FlatButton"
+script = ExtResource("1_q6blt")
+
+[node name="RichTextLabel" type="RichTextLabel" parent="."]
+layout_mode = 1
+anchors_preset = 14
+anchor_top = 0.5
+anchor_right = 1.0
+anchor_bottom = 0.5
+offset_left = 6.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+bbcode_enabled = true
+fit_content = true
diff --git a/src/ui_elements/path_command_editor.gd b/src/ui_elements/path_command_editor.gd
new file mode 100644
index 000000000..d679cc1c5
--- /dev/null
+++ b/src/ui_elements/path_command_editor.gd
@@ -0,0 +1,327 @@
+## An editor for a single path command.
+extends Control
+
+@export var absolute_button_normal: StyleBoxFlat
+@export var absolute_button_hovered: StyleBoxFlat
+@export var absolute_button_pressed: StyleBoxFlat
+@export var relative_button_normal: StyleBoxFlat
+@export var relative_button_hovered: StyleBoxFlat
+@export var relative_button_pressed: StyleBoxFlat
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const MiniNumberField = preload("mini_number_field.tscn")
+const FlagField = preload("flag_field.tscn")
+const PathCommandPopup = preload("res://src/ui_elements/path_popup.tscn")
+
+const code_font = preload("res://visual/fonts/FontMono.ttf")
+const more_icon = preload("res://visual/icons/SmallMore.svg")
+
+var tid := PackedInt32Array()
+var cmd_char := ""
+var cmd_idx := -1
+var path_command: PathCommand
+
+var active := false
+@onready var relative_button: Button
+@onready var action_button: Button
+var fields: Array[Control] = []
+
+
+func update_value(new_value: float, property: StringName) -> void:
+ get_path_attribute().set_command_property(cmd_idx, property, new_value)
+
+func _on_relative_button_pressed() -> void:
+ get_path_attribute().toggle_relative_command(cmd_idx)
+
+
+func _ready() -> void:
+ cmd_char = path_command.command_char
+ Indications.selection_changed.connect(determine_selection_state)
+ Indications.hover_changed.connect(determine_selection_state)
+ determine_selection_state()
+
+
+func add_numfield() -> BetterLineEdit:
+ var new_field := MiniNumberField.instantiate()
+ new_field.focus_entered.connect(Indications.normal_select.bind(tid, cmd_idx))
+ return new_field
+
+
+func _gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion and event.button_mask == 0:
+ Indications.set_hovered(tid, cmd_idx)
+ elif event is InputEventMouseButton and event.is_pressed():
+ if event.button_index == MOUSE_BUTTON_LEFT:
+ if event.double_click:
+ # Unselect the tag, so then it's selected again.
+ Indications.ctrl_select(tid, cmd_idx)
+ var subpath_range: Vector2i =\
+ SVG.root_tag.get_tag(tid).attributes.d.get_subpath(cmd_idx)
+ for idx in range(subpath_range.x, subpath_range.y + 1):
+ Indications.ctrl_select(tid, idx)
+ elif event.is_command_or_control_pressed():
+ Indications.ctrl_select(tid, cmd_idx)
+ elif event.shift_pressed:
+ Indications.shift_select(tid, cmd_idx)
+ else:
+ Indications.normal_select(tid, cmd_idx)
+ elif event.button_index == MOUSE_BUTTON_RIGHT:
+ if Indications.semi_selected_tid != tid or\
+ not cmd_idx in Indications.inner_selections:
+ Indications.normal_select(tid, cmd_idx)
+ # Popup the actions.
+ var viewport := get_viewport()
+ var popup_pos := viewport.get_mouse_position()
+ Utils.popup_under_pos(Indications.get_selection_context(
+ Utils.popup_under_pos.bind(popup_pos, viewport)), popup_pos, viewport)
+
+
+var current_interaction_state := Utils.InteractionType.NONE
+
+func determine_selection_state() -> void:
+ var new_interaction_state := Utils.InteractionType.NONE
+ if Indications.semi_selected_tid == tid and cmd_idx in Indications.inner_selections:
+ if Indications.semi_hovered_tid == tid and Indications.inner_hovered == cmd_idx:
+ new_interaction_state = Utils.InteractionType.HOVERED_SELECTED
+ else:
+ new_interaction_state = Utils.InteractionType.SELECTED
+ elif Indications.semi_hovered_tid == tid and Indications.inner_hovered == cmd_idx:
+ new_interaction_state = Utils.InteractionType.HOVERED
+
+ if current_interaction_state != new_interaction_state:
+ current_interaction_state = new_interaction_state
+ queue_redraw()
+
+func _draw() -> void:
+ # First draw interaction-based stuff, as the highlight is behind everything.
+ if current_interaction_state != Utils.InteractionType.NONE:
+ var stylebox := StyleBoxFlat.new()
+ stylebox.set_corner_radius_all(3)
+ if current_interaction_state == Utils.InteractionType.HOVERED:
+ stylebox.bg_color = Color(0.8, 0.8, 1.0, 0.05)
+ elif current_interaction_state == Utils.InteractionType.SELECTED:
+ stylebox.bg_color = Color(0.6, 0.6, 1.0, 0.16)
+ elif current_interaction_state == Utils.InteractionType.HOVERED_SELECTED:
+ stylebox.bg_color = Color(0.7, 0.7, 1.0, 0.18)
+ stylebox.draw(get_canvas_item(), Rect2(Vector2.ZERO, size))
+ # Draw the child controls. They are going to be drawn, not added as a node unless
+ # the mouse hovers them. This is a hack to significantly improve performance.
+ if not active:
+ # Draw the relative/absolute button.
+ var relative_button_rect := Rect2(Vector2(3, 2), Vector2(18, size.y - 4))
+ draw_style_box(absolute_button_normal if Utils.is_string_upper(cmd_char) else\
+ relative_button_normal, relative_button_rect)
+ draw_string(code_font, Vector2(6, size.y - 6), cmd_char,
+ HORIZONTAL_ALIGNMENT_CENTER, 12, 13)
+ # Draw the action button.
+ draw_texture_rect(more_icon, Rect2(Vector2(size.x - 19, 4),
+ Vector2(14, 14)), false, Color("bfbfbf"))
+ # Draw the fields.
+ match cmd_char.to_upper():
+ "A":
+ # Because of the flag editors, the procedure is as simple as for the rest.
+ var stylebox := get_theme_stylebox(&"normal", &"MiniLineEdit")
+ var font_size := get_theme_font_size(&"font_size", &"MiniLineEdit")
+ var font_color := get_theme_color(&"font_outline_color", &"MiniLineEdit")
+ var rect := Rect2(Vector2(25, 2), Vector2(44, 18))
+ draw_numfield(rect, stylebox, &"rx", font_size, font_color)
+ rect.position.x = rect.end.x + 3
+ draw_numfield(rect, stylebox, &"ry", font_size, font_color)
+ rect.position.x = rect.end.x + 4
+ draw_numfield(rect, stylebox, &"rot", font_size, font_color)
+ rect.position.x = rect.end.x + 4
+ rect.size.x = 19
+ var flag_field := FlagField.instantiate()
+ draw_style_box(flag_field.get_theme_stylebox(&"normal" if\
+ path_command.large_arc_flag == 0 else &"pressed"), rect)
+ draw_string(code_font, rect.position + Vector2(5, 14),
+ String.num_uint64(path_command.large_arc_flag),
+ HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, 14,
+ flag_field.get_theme_color(&"font_color" if\
+ path_command.large_arc_flag == 0 else &"font_pressed_color"))
+ rect.position.x = rect.end.x + 4
+ draw_style_box(flag_field.get_theme_stylebox(&"normal" if\
+ path_command.sweep_flag == 0 else &"pressed"), rect)
+ draw_string(code_font, rect.position + Vector2(5, 14),
+ String.num_uint64(path_command.sweep_flag),
+ HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, 14,
+ flag_field.get_theme_color(&"font_color" if\
+ path_command.sweep_flag == 0 else &"font_pressed_color"))
+ flag_field.free()
+ rect.position.x = rect.end.x + 4
+ rect.size.x = 44
+ draw_numfield(rect, stylebox, &"x", font_size, font_color)
+ rect.position.x = rect.end.x + 3
+ draw_numfield(rect, stylebox, &"y", font_size, font_color)
+ "C": draw_numfield_arr([3, 4, 3, 4, 3], [&"x1", &"y1", &"x2", &"y2", &"x", &"y"])
+ "Q": draw_numfield_arr([3, 4, 3], [&"x1", &"y1", &"x", &"y"])
+ "S": draw_numfield_arr([3, 4, 3], [&"x2", &"y2", &"x", &"y"])
+ "M", "L", "T": draw_numfield_arr([3], [&"x", &"y"])
+ "H":
+ var stylebox := get_theme_stylebox(&"normal", &"MiniLineEdit")
+ var font_size := get_theme_font_size(&"font_size", &"MiniLineEdit")
+ var font_color := get_theme_color(&"font_outline_color", &"MiniLineEdit")
+ var rect := Rect2(Vector2(25, 2), Vector2(44, 18))
+ draw_numfield(rect, stylebox, &"x", font_size, font_color)
+ "V":
+ var stylebox := get_theme_stylebox(&"normal", &"MiniLineEdit")
+ var font_size := get_theme_font_size(&"font_size", &"MiniLineEdit")
+ var font_color := get_theme_color(&"font_outline_color", &"MiniLineEdit")
+ var rect := Rect2(Vector2(25, 2), Vector2(44, 18))
+ draw_numfield(rect, stylebox, &"y", font_size, font_color)
+
+func draw_numfield(rect: Rect2, stylebox: StyleBoxFlat, property: StringName,\
+font_size: int, font_color: Color) -> void:
+ draw_style_box(stylebox, rect)
+ draw_string(code_font, rect.position + Vector2(4, 13),
+ NumberArrayParser.basic_num_to_text(path_command.get(property)),
+ HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 4, font_size, font_color)
+
+func draw_numfield_arr(spacings: Array, names: Array[StringName]) -> void:
+ var stylebox := get_theme_stylebox(&"normal", &"MiniLineEdit")
+ var font_size := get_theme_font_size(&"font_size", &"MiniLineEdit")
+ var font_color := get_theme_color(&"font_outline_color", &"MiniLineEdit")
+ var rect := Rect2(Vector2(25, 2), Vector2(44, 18))
+ draw_numfield(rect, stylebox, names[0], font_size, font_color)
+ for i in spacings.size():
+ rect.position.x = rect.end.x + spacings[i]
+ draw_numfield(rect, stylebox, names[i + 1], font_size, font_color)
+
+# Prevents the relative button from selecting a whole subpath when double-clicked.
+func _on_relative_button_gui_input(event: InputEvent) -> void:
+ if active:
+ if event is InputEventMouseButton and event.double_click:
+ relative_button.accept_event()
+ relative_button.pressed.emit()
+
+# Prevents the action button from selecting a whole subpath when double-clicked.
+func _on_action_button_gui_input(event: InputEvent) -> void:
+ if active:
+ if event is InputEventMouseButton and event.double_click:
+ action_button.accept_event()
+ action_button.pressed.emit()
+
+# When the mouse enters the path command editor, activate it by adding the real nodes.
+# Otherwise, the nodes should only be drawn. This is important for performance.
+func _on_mouse_entered() -> void:
+ if active:
+ return
+
+ active = true
+ # Setup the relative button.
+ relative_button = Button.new()
+ relative_button.focus_mode = Control.FOCUS_NONE
+ relative_button.mouse_filter = Control.MOUSE_FILTER_PASS
+ relative_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ relative_button.text = cmd_char
+ relative_button.begin_bulk_theme_override()
+ relative_button.add_theme_font_override(&"font", code_font)
+ relative_button.add_theme_font_size_override(&"font_size", 13)
+ relative_button.add_theme_color_override(&"font_color", Color(1, 1, 1))
+ if Utils.is_string_upper(cmd_char):
+ relative_button.tooltip_text = "%s (%s)" %\
+ [Utils.path_command_char_dict[cmd_char.to_upper()], tr(&"absolute")]
+ relative_button.add_theme_stylebox_override(&"normal", absolute_button_normal)
+ relative_button.add_theme_stylebox_override(&"hover", absolute_button_hovered)
+ relative_button.add_theme_stylebox_override(&"pressed", absolute_button_pressed)
+ else:
+ relative_button.tooltip_text = "%s (%s)" %\
+ [Utils.path_command_char_dict[cmd_char.to_upper()], tr(&"relative")]
+ relative_button.add_theme_stylebox_override(&"normal", relative_button_normal)
+ relative_button.add_theme_stylebox_override(&"hover", relative_button_hovered)
+ relative_button.add_theme_stylebox_override(&"pressed", relative_button_pressed)
+ relative_button.end_bulk_theme_override()
+ add_child(relative_button)
+ relative_button.pressed.connect(_on_relative_button_pressed)
+ relative_button.gui_input.connect(_on_relative_button_gui_input)
+ relative_button.position = Vector2(3, 2)
+ relative_button.size = Vector2(18, size.y - 4)
+ # Setup the action button.
+ action_button = Button.new()
+ action_button.icon = more_icon
+ action_button.theme_type_variation = &"FlatButton"
+ action_button.focus_mode = Control.FOCUS_NONE
+ action_button.mouse_filter = Control.MOUSE_FILTER_PASS
+ action_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ add_child(action_button)
+ action_button.pressed.connect(_on_action_button_pressed)
+ action_button.gui_input.connect(_on_action_button_gui_input)
+ action_button.position = Vector2(size.x - 21, 2)
+ action_button.size = Vector2(size.y - 4, size.y - 4)
+ # Setup the fields.
+ match cmd_char.to_upper():
+ "A":
+ var field_rx: BetterLineEdit = add_numfield()
+ var field_ry: BetterLineEdit = add_numfield()
+ var field_rot: BetterLineEdit = add_numfield()
+ field_rx.mode = field_rx.Mode.ONLY_POSITIVE
+ field_ry.mode = field_ry.Mode.ONLY_POSITIVE
+ field_rot.mode = field_rot.Mode.HALF_ANGLE
+ fields = [field_rx, field_ry, field_rot, FlagField.instantiate(),
+ FlagField.instantiate(), add_numfield(), add_numfield()]
+ setup_fields([3, 4, 4, 4, 4, 3],
+ ["rx", "ry", "rot", "large_arc_flag", "sweep_flag", "x", "y"])
+ "C":
+ fields = [add_numfield(), add_numfield(), add_numfield(), add_numfield(),
+ add_numfield(), add_numfield()]
+ setup_fields([3, 4, 3, 4, 3], ["x1", "y1", "x2", "y2", "x", "y"])
+ "Q":
+ fields = [add_numfield(), add_numfield(), add_numfield(), add_numfield()]
+ setup_fields([3, 4, 3], ["x1", "y1", "x", "y"])
+ "S":
+ fields = [add_numfield(), add_numfield(), add_numfield(), add_numfield()]
+ setup_fields([3, 4, 3], ["x2", "y2", "x", "y"])
+ "M", "L", "T":
+ fields = [add_numfield(), add_numfield()]
+ setup_fields([3], ["x", "y"])
+ "H":
+ fields = [add_numfield()]
+ setup_fields([], ["x"])
+ "V":
+ fields = [add_numfield()]
+ setup_fields([], ["y"])
+ "Z": fields.clear()
+ # Remove the graphics, as now there are real nodes.
+ queue_redraw()
+
+func setup_fields(spacings: Array, names: Array) -> void:
+ for i in fields.size():
+ var property_string: String = names[i]
+ var property_stringname := StringName(property_string)
+ fields[i].set_value(path_command.get(property_stringname))
+ fields[i].tooltip_text = property_string
+ fields[i].value_changed.connect(update_value.bind(property_stringname))
+ add_child(fields[i])
+ fields[i].position.y = 2
+
+ fields[0].position.x = 25
+ for i in fields.size() - 1:
+ fields[i + 1].position.x = fields[i].get_end().x + spacings[i]
+
+func _on_mouse_exited() -> void:
+ Indications.remove_hovered(tid, cmd_idx)
+
+ if active:
+ active = false
+ for field in fields:
+ if field.has_focus():
+ active = true
+ # Should switch out the controls for fake outs. This is safe even when
+ # you've focused a BetterLineEdit, because it pauses the tree.
+ if not active:
+ clear_children()
+ queue_redraw()
+
+func clear_children() -> void:
+ for child in get_children():
+ child.queue_free()
+
+func _on_action_button_pressed() -> void:
+ var viewport := get_viewport()
+ var action_button_rect := action_button.get_global_rect()
+ Utils.popup_under_rect_center(Indications.get_selection_context(
+ Utils.popup_under_rect_center.bind(action_button_rect, viewport)),
+ action_button_rect, viewport)
+
+func get_path_attribute() -> AttributePath:
+ return SVG.root_tag.get_tag(tid).attributes.d
diff --git a/src/ui_elements/path_command_editor.tscn b/src/ui_elements/path_command_editor.tscn
new file mode 100644
index 000000000..32d2f641c
--- /dev/null
+++ b/src/ui_elements/path_command_editor.tscn
@@ -0,0 +1,116 @@
+[gd_scene load_steps=8 format=3 uid="uid://dcdrc3r60bgg3"]
+
+[ext_resource type="Script" path="res://src/ui_elements/path_command_editor.gd" id="1_om2fk"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g5128"]
+content_margin_left = 5.0
+content_margin_top = 0.0
+content_margin_right = 5.0
+content_margin_bottom = 0.0
+bg_color = Color(0.8, 0.48, 0.16, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.9, 0.684, 0.36, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xysdk"]
+content_margin_left = 5.0
+content_margin_top = 0.0
+content_margin_right = 5.0
+content_margin_bottom = 0.0
+bg_color = Color(0.85, 0.51, 0.17, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.95, 0.798, 0.57, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sbcff"]
+content_margin_left = 5.0
+content_margin_top = 0.0
+content_margin_right = 5.0
+content_margin_bottom = 0.0
+bg_color = Color(1, 0.75, 0.25, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 0.925, 0.7, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gs8ph"]
+content_margin_left = 5.0
+content_margin_top = 0.0
+content_margin_right = 5.0
+content_margin_bottom = 0.0
+bg_color = Color(0.64, 0.16, 0.8, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.7425, 0.45, 0.9, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tyaol"]
+content_margin_left = 5.0
+content_margin_top = 0.0
+content_margin_right = 5.0
+content_margin_bottom = 0.0
+bg_color = Color(0.678431, 0.168627, 0.85098, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.823333, 0.57, 0.95, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cbymw"]
+content_margin_left = 5.0
+content_margin_top = 0.0
+content_margin_right = 5.0
+content_margin_bottom = 0.0
+bg_color = Color(0.75, 0.25, 1, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.875, 0.7, 1, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[node name="PathCommandEditor" type="Control"]
+custom_minimum_size = Vector2(0, 22)
+layout_mode = 3
+anchors_preset = 0
+offset_right = 38.0
+offset_bottom = 14.0
+script = ExtResource("1_om2fk")
+absolute_button_normal = SubResource("StyleBoxFlat_g5128")
+absolute_button_hovered = SubResource("StyleBoxFlat_xysdk")
+absolute_button_pressed = SubResource("StyleBoxFlat_sbcff")
+relative_button_normal = SubResource("StyleBoxFlat_gs8ph")
+relative_button_hovered = SubResource("StyleBoxFlat_tyaol")
+relative_button_pressed = SubResource("StyleBoxFlat_cbymw")
+
+[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"]
+[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
diff --git a/src/ui_elements/path_field.gd b/src/ui_elements/path_field.gd
new file mode 100644
index 000000000..ca9698e79
--- /dev/null
+++ b/src/ui_elements/path_field.gd
@@ -0,0 +1,75 @@
+## An editor to be tied to an AttributePath.
+extends VBoxContainer
+
+signal focused
+var attribute: AttributePath
+var attribute_name: String
+
+const CommandEditor = preload("path_command_editor.tscn")
+
+@onready var line_edit: LineEdit = $LineEdit
+@onready var commands_container: VBoxContainer = $HBox/Commands
+@onready var add_move: Button = $AddMove
+
+func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR):
+ sync(attribute.autoformat(new_value))
+ if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
+ match update_type:
+ Utils.UpdateType.INTERMEDIATE:
+ attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
+ Utils.UpdateType.FINAL:
+ attribute.set_value(new_value, Attribute.SyncMode.FINAL)
+ _:
+ attribute.set_value(new_value)
+
+func _ready() -> void:
+ set_value(attribute.get_value())
+ attribute.value_changed.connect(set_value)
+ line_edit.tooltip_text = attribute_name
+
+
+func rebuild_commands() -> void:
+ var command_idx := 0
+ for command_editor in commands_container.get_children():
+ if command_idx >= attribute.get_command_count():
+ command_editor.queue_free()
+ else:
+ var command: PathCommand = attribute.get_command(command_idx)
+ if command_editor.cmd_char == command.command_char:
+ command_editor.path_command = command
+ command_editor.queue_redraw()
+ else:
+ var new_command_editor := CommandEditor.instantiate()
+ new_command_editor.path_command = command
+ # TODO Fix this mess, it's needed for individual path commands selection.
+ new_command_editor.tid = get_node(^"../../../../..").tid
+ new_command_editor.cmd_idx = command_idx
+ command_editor.clear_children()
+ command_editor.replace_by(new_command_editor)
+ command_editor.queue_free()
+ command_idx += 1
+
+ while command_idx < attribute.get_command_count():
+ var command_editor := CommandEditor.instantiate()
+ command_editor.path_command = attribute.get_command(command_idx)
+ # TODO Fix this mess, it's needed for individual path commands selection.
+ command_editor.tid = get_node(^"../../../../..").tid
+ command_editor.cmd_idx = command_idx
+ commands_container.add_child(command_editor)
+ command_idx += 1
+
+
+func _on_line_edit_text_submitted(new_text: String) -> void:
+ set_value(new_text)
+
+func _on_line_edit_focus_entered() -> void:
+ focused.emit()
+
+func _on_add_move_pressed() -> void:
+ attribute.insert_command(0, "M")
+
+func sync(new_value: String) -> void:
+ line_edit.text = new_value
+ # A plus button for adding a move command if empty.
+ add_move.visible = (attribute.get_command_count() == 0)
+ rebuild_commands()
diff --git a/src/ui_elements/path_field.tscn b/src/ui_elements/path_field.tscn
new file mode 100644
index 000000000..73f072ebc
--- /dev/null
+++ b/src/ui_elements/path_field.tscn
@@ -0,0 +1,103 @@
+[gd_scene load_steps=9 format=3 uid="uid://dqy5lv33sy5r7"]
+
+[ext_resource type="Script" path="res://src/ui_elements/path_field.gd" id="1_22rk2"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="2_48xgh"]
+[ext_resource type="Texture2D" uid="uid://eif2ioi0mw17" path="res://visual/icons/Plus.svg" id="3_at2g1"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4rmxx"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wqlhg"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_lcnxl"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4yine"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kmpxk"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(1, 1, 1, 0.2)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[node name="PathField" type="VBoxContainer"]
+offset_right = 336.0
+offset_bottom = 45.0
+theme_override_constants/separation = 2
+script = ExtResource("1_22rk2")
+
+[node name="LineEdit" type="LineEdit" parent="."]
+custom_minimum_size = Vector2(352, 0)
+layout_mode = 2
+size_flags_horizontal = 0
+focus_mode = 1
+placeholder_text = "No path data"
+script = ExtResource("2_48xgh")
+hover_stylebox = SubResource("StyleBoxFlat_4rmxx")
+focus_stylebox = SubResource("StyleBoxFlat_wqlhg")
+code_font_tooltip = true
+
+[node name="AddMove" type="Button" parent="."]
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 0
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_override_styles/normal = SubResource("StyleBoxEmpty_lcnxl")
+theme_override_styles/hover = SubResource("StyleBoxFlat_4yine")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_kmpxk")
+icon = ExtResource("3_at2g1")
+
+[node name="HBox" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Spacer" type="Control" parent="HBox"]
+custom_minimum_size = Vector2(8, 0)
+layout_mode = 2
+mouse_filter = 1
+
+[node name="Commands" type="VBoxContainer" parent="HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+mouse_filter = 0
+theme_override_constants/separation = 0
+
+[connection signal="focus_entered" from="LineEdit" to="." method="_on_line_edit_focus_entered"]
+[connection signal="text_submitted" from="LineEdit" to="." method="_on_line_edit_text_submitted"]
+[connection signal="pressed" from="AddMove" to="." method="_on_add_move_pressed"]
diff --git a/src/ui_elements/path_popup.gd b/src/ui_elements/path_popup.gd
new file mode 100644
index 000000000..5f1d248fd
--- /dev/null
+++ b/src/ui_elements/path_popup.gd
@@ -0,0 +1,46 @@
+## A popup for picking a path command.
+extends Popup
+
+signal path_command_picked(new_command: String)
+
+@onready var command_container: VBoxContainer = %CommandContainer
+@onready var relative_toggle: CheckButton = %RelativeToggle
+@onready var vbox: VBoxContainer = $PanelContainer/VBoxContainer
+@onready var top_margin: MarginContainer = $PanelContainer/VBoxContainer/MarginContainer
+
+func _ready() -> void:
+ relative_toggle.toggled.connect(_on_relative_toggle_toggled)
+ relative_toggle.button_pressed = GlobalSettings.save_data.path_command_relative
+ for command_button in command_container.get_children():
+ command_button.pressed_custom.connect(emit_picked)
+
+func emit_picked(cmd_char: String) -> void:
+ path_command_picked.emit(cmd_char)
+ hide()
+
+func _on_relative_toggle_toggled(toggled_on: bool) -> void:
+ GlobalSettings.modify_save_data("path_command_relative", toggled_on)
+ for command_button in command_container.get_children():
+ if toggled_on:
+ command_button.command_char = command_button.command_char.to_lower()
+ else:
+ command_button.command_char = command_button.command_char.to_upper()
+ command_button.update_text()
+
+func disable_invalid(cmd_chars: Array) -> void:
+ for cmd_char in cmd_chars:
+ var cmd_char_upper: String = cmd_char.to_upper()
+ command_container.get_node(cmd_char_upper).set_invalid()
+
+func force_relativity(relative: bool) -> void:
+ relative_toggle.hide()
+ vbox.add_theme_constant_override(&"separation", 0)
+ top_margin.add_theme_constant_override(&"margin_top", 0)
+ for command_button in command_container.get_children():
+ if relative:
+ command_button.command_char = command_button.command_char.to_lower()
+ command_button.update_text()
+ else:
+ command_button.command_char = command_button.command_char.to_upper()
+ command_button.update_text()
+ reset_size()
diff --git a/src/ui_elements/path_popup.tscn b/src/ui_elements/path_popup.tscn
new file mode 100644
index 000000000..80e515a63
--- /dev/null
+++ b/src/ui_elements/path_popup.tscn
@@ -0,0 +1,93 @@
+[gd_scene load_steps=3 format=3 uid="uid://bvnheiqqay5ke"]
+
+[ext_resource type="Script" path="res://src/ui_elements/path_popup.gd" id="1_j10aq"]
+[ext_resource type="PackedScene" uid="uid://co2btefrqrm0e" path="res://src/ui_elements/path_command_button.tscn" id="2_1jd8y"]
+
+[node name="PathCommandPopup" type="Popup"]
+transparent_bg = true
+size = Vector2i(236, 282)
+visible = true
+script = ExtResource("1_j10aq")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+custom_minimum_size = Vector2(236, 0)
+offset_right = 106.0
+offset_bottom = 97.0
+
+[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"]
+layout_mode = 2
+theme_override_constants/separation = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/margin_top = 2
+theme_override_constants/margin_right = 4
+
+[node name="MainContainer" type="VBoxContainer" parent="PanelContainer/VBoxContainer/MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 2
+
+[node name="RelativeToggle" type="CheckButton" parent="PanelContainer/VBoxContainer/MarginContainer/MainContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 8
+focus_mode = 0
+mouse_default_cursor_shape = 2
+text = "#relative"
+flat = true
+alignment = 2
+
+[node name="CommandContainer" type="VBoxContainer" parent="PanelContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="M" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "M"
+
+[node name="L" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "L"
+
+[node name="H" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "H"
+
+[node name="V" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "V"
+
+[node name="Z" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "Z"
+
+[node name="A" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "A"
+
+[node name="Q" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "Q"
+
+[node name="T" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "T"
+
+[node name="C" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "C"
+
+[node name="S" parent="PanelContainer/VBoxContainer/CommandContainer" instance=ExtResource("2_1jd8y")]
+layout_mode = 2
+focus_mode = 0
+command_char = "S"
diff --git a/src/ui_elements/setting_check_box.gd b/src/ui_elements/setting_check_box.gd
new file mode 100644
index 000000000..d4e84cdcf
--- /dev/null
+++ b/src/ui_elements/setting_check_box.gd
@@ -0,0 +1,10 @@
+extends CheckBox
+
+@export var section_name: String
+@export var setting_name: String
+
+func _ready() -> void:
+ button_pressed = GlobalSettings.get(setting_name)
+
+func _on_pressed() -> void:
+ GlobalSettings.toggle_bool_setting(section_name, setting_name)
diff --git a/src/ui_elements/setting_check_box.tscn b/src/ui_elements/setting_check_box.tscn
new file mode 100644
index 000000000..4009766e4
--- /dev/null
+++ b/src/ui_elements/setting_check_box.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=2 format=3 uid="uid://ctsee23lxlrib"]
+
+[ext_resource type="Script" path="res://src/ui_elements/setting_check_box.gd" id="1_xy5x4"]
+
+[node name="SettingCheckBox" type="CheckBox"]
+offset_right = 24.0
+offset_bottom = 20.0
+focus_mode = 0
+mouse_default_cursor_shape = 2
+script = ExtResource("1_xy5x4")
+
+[connection signal="pressed" from="." to="." method="_on_pressed"]
diff --git a/src/ui_elements/transform_editor.gd b/src/ui_elements/transform_editor.gd
new file mode 100644
index 000000000..8eed1b78d
--- /dev/null
+++ b/src/ui_elements/transform_editor.gd
@@ -0,0 +1,10 @@
+extends PanelContainer
+
+var type: String
+
+@onready var transform_list: VBoxContainer = $TransformList
+@onready var transform_icon: TextureRect = $TransformList/TopPanel/TransformIcon
+@onready var transform_label: Label = $TransformList/TopPanel/TransformLabel
+@onready var more_button: Button = $TransformList/TopPanel/MoreButton
+
+var fields: Array[BetterLineEdit]
diff --git a/src/ui_elements/transform_editor.tscn b/src/ui_elements/transform_editor.tscn
new file mode 100644
index 000000000..4db5df61c
--- /dev/null
+++ b/src/ui_elements/transform_editor.tscn
@@ -0,0 +1,47 @@
+[gd_scene load_steps=5 format=3 uid="uid://c7xhf7bodwnl1"]
+
+[ext_resource type="Script" path="res://src/ui_elements/transform_editor.gd" id="1_q14io"]
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="2_84xsl"]
+[ext_resource type="Texture2D" uid="uid://cmepkbqde0jh0" path="res://visual/icons/SmallMore.svg" id="3_2ssf5"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_af5dq"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 4.0
+bg_color = Color(0.866667, 0.933333, 1, 0.0666667)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[node name="TransformEditor" type="PanelContainer"]
+offset_right = 31.0
+offset_bottom = 24.0
+theme_override_styles/panel = SubResource("StyleBoxFlat_af5dq")
+script = ExtResource("1_q14io")
+
+[node name="TransformList" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="TopPanel" type="HBoxContainer" parent="TransformList"]
+layout_mode = 2
+alignment = 1
+
+[node name="TransformIcon" type="TextureRect" parent="TransformList/TopPanel"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 0
+
+[node name="TransformLabel" type="Label" parent="TransformList/TopPanel"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.866667, 0.933333, 1, 0.933333)
+theme_override_fonts/font = ExtResource("2_84xsl")
+theme_override_font_sizes/font_size = 13
+
+[node name="MoreButton" type="Button" parent="TransformList/TopPanel"]
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"FlatButton"
+icon = ExtResource("3_2ssf5")
diff --git a/src/ui_elements/transform_field.gd b/src/ui_elements/transform_field.gd
new file mode 100644
index 000000000..ea24ecbbf
--- /dev/null
+++ b/src/ui_elements/transform_field.gd
@@ -0,0 +1,59 @@
+## An editor to be tied to a transform attribute.
+extends HBoxContainer
+
+signal focused
+var attribute: AttributeTransform
+var attribute_name: String
+
+const TransformPopup = preload("res://src/ui_elements/transform_popup.tscn")
+
+@onready var line_edit: BetterLineEdit = $LineEdit
+@onready var popup_button: Button = $Button
+
+func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+ sync(attribute.autoformat(new_value))
+ if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
+ match update_type:
+ Utils.UpdateType.INTERMEDIATE:
+ attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
+ Utils.UpdateType.FINAL:
+ attribute.set_value(new_value, Attribute.SyncMode.FINAL)
+ _:
+ attribute.set_value(new_value)
+
+func set_num(new_number: float, update_type := Utils.UpdateType.REGULAR) -> void:
+ set_value(NumberParser.num_to_text(new_number), update_type)
+
+func _ready() -> void:
+ set_value(attribute.get_value())
+ attribute.value_changed.connect(set_value)
+ line_edit.tooltip_text = attribute_name
+
+func _on_focus_entered() -> void:
+ focused.emit()
+
+func _on_text_submitted(submitted_text: String) -> void:
+ set_value(submitted_text)
+
+func _on_matrix_popup_edited(new_matrix: String) -> void:
+ set_value(new_matrix)
+
+func sync(new_value: String) -> void:
+ line_edit.text = new_value
+
+func _on_button_pressed() -> void:
+ var transform_popup := TransformPopup.instantiate()
+ transform_popup.attribute_ref = attribute
+ add_child(transform_popup)
+ Utils.popup_under_rect(transform_popup, line_edit.get_global_rect(), get_viewport())
+
+
+func _on_button_gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and\
+ event.is_pressed():
+ accept_event()
+ var mouse_motion_event := InputEventMouseMotion.new()
+ mouse_motion_event.position = get_viewport().get_mouse_position()
+ Input.parse_input_event(mouse_motion_event)
+ else:
+ popup_button.mouse_filter = Utils.mouse_filter_pass_non_drag_events(event)
diff --git a/src/ui_elements/transform_field.tscn b/src/ui_elements/transform_field.tscn
new file mode 100644
index 000000000..6b64fa4ce
--- /dev/null
+++ b/src/ui_elements/transform_field.tscn
@@ -0,0 +1,57 @@
+[gd_scene load_steps=6 format=3 uid="uid://bc45fp38kar6a"]
+
+[ext_resource type="Script" path="res://src/ui_elements/transform_field.gd" id="1_5gokr"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="2_aucxm"]
+[ext_resource type="Texture2D" uid="uid://coda6chhcatal" path="res://visual/icons/Arrow.svg" id="3_gqs5i"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ynuh7"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h7r5w"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[node name="TransformField" type="HBoxContainer"]
+custom_minimum_size = Vector2(128, 0)
+theme_override_constants/separation = 0
+script = ExtResource("1_5gokr")
+
+[node name="LineEdit" type="LineEdit" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_mode = 1
+mouse_filter = 1
+theme_type_variation = &"RightConnectedLineEdit"
+placeholder_text = "No transforms"
+script = ExtResource("2_aucxm")
+hover_stylebox = SubResource("StyleBoxFlat_ynuh7")
+focus_stylebox = SubResource("StyleBoxFlat_h7r5w")
+code_font_tooltip = true
+
+[node name="Button" type="Button" parent="."]
+custom_minimum_size = Vector2(15, 0)
+layout_mode = 2
+focus_mode = 0
+mouse_filter = 1
+mouse_default_cursor_shape = 2
+theme_type_variation = &"LeftConnectedButton"
+icon = ExtResource("3_gqs5i")
+expand_icon = true
+
+[connection signal="focus_entered" from="LineEdit" to="." method="_on_focus_entered"]
+[connection signal="text_submitted" from="LineEdit" to="." method="_on_text_submitted"]
+[connection signal="gui_input" from="Button" to="." method="_on_button_gui_input"]
+[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
diff --git a/src/ui_elements/transform_popup.gd b/src/ui_elements/transform_popup.gd
new file mode 100644
index 000000000..3ded31e32
--- /dev/null
+++ b/src/ui_elements/transform_popup.gd
@@ -0,0 +1,286 @@
+## A popup for editing a transform matrix.
+extends Popup
+
+const NumberEditType = preload("res://src/ui_elements/number_edit.gd")
+const ContextPopupType = preload("res://src/ui_elements/context_popup.gd")
+
+const MiniNumberField = preload("res://src/ui_elements/mini_number_field.tscn")
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const TransformEditor = preload("res://src/ui_elements/transform_editor.tscn")
+const code_font = preload("res://visual/fonts/FontMono.ttf")
+
+const icons_dict := {
+ "matrix": preload("res://visual/icons/Matrix.svg"),
+ "translate": preload("res://visual/icons/Translate.svg"),
+ "rotate": preload("res://visual/icons/Rotate.svg"),
+ "scale": preload("res://visual/icons/Scale.svg"),
+ "skewX": preload("res://visual/icons/SkewX.svg"),
+ "skewY": preload("res://visual/icons/SkewY.svg"),
+}
+
+var attribute_ref: AttributeTransform
+var UR := UndoRedo.new()
+
+@onready var x1_edit: NumberEditType = %FinalMatrix/X1
+@onready var x2_edit: NumberEditType = %FinalMatrix/X2
+@onready var y1_edit: NumberEditType = %FinalMatrix/Y1
+@onready var y2_edit: NumberEditType = %FinalMatrix/Y2
+@onready var o1_edit: NumberEditType = %FinalMatrix/O1
+@onready var o2_edit: NumberEditType = %FinalMatrix/O2
+@onready var transform_list: VBoxContainer = %TransformList
+@onready var add_button: Button = %AddButton
+
+func _ready() -> void:
+ add_button.pressed.connect(popup_new_transform_context.bind(0, add_button))
+ rebuild()
+
+func rebuild() -> void:
+ var transform_count := attribute_ref.get_transform_count()
+ # Sync until the first different transform type is found; then rebuild the rest.
+ var i := 0
+ for transform_editor in transform_list.get_children():
+ if i >= transform_count:
+ break
+ var t := attribute_ref.get_transform(i)
+ if t is AttributeTransform.TransformMatrix and transform_editor.type == "matrix":
+ transform_editor.fields[0].set_value(t.x1, true)
+ transform_editor.fields[1].set_value(t.x2, true)
+ transform_editor.fields[2].set_value(t.y1, true)
+ transform_editor.fields[3].set_value(t.y2, true)
+ transform_editor.fields[4].set_value(t.o1, true)
+ transform_editor.fields[5].set_value(t.o2, true)
+ elif t is AttributeTransform.TransformTranslate and\
+ transform_editor.type == "translate":
+ transform_editor.fields[0].set_value(t.x, true)
+ transform_editor.fields[1].set_value(t.y, true)
+ elif t is AttributeTransform.TransformRotate and transform_editor.type == "rotate":
+ transform_editor.fields[0].set_value(t.deg, true)
+ transform_editor.fields[1].set_value(t.x, true)
+ transform_editor.fields[2].set_value(t.y, true)
+ elif t is AttributeTransform.TransformScale and transform_editor.type == "scale":
+ transform_editor.fields[0].set_value(t.x, true)
+ transform_editor.fields[1].set_value(t.y, true)
+ elif t is AttributeTransform.TransformSkewX and transform_editor.type == "skewX":
+ transform_editor.fields[0].set_value(t.x, true)
+ elif t is AttributeTransform.TransformSkewY and transform_editor.type == "skewY":
+ transform_editor.fields[0].set_value(t.y, true)
+ else:
+ break
+ i += 1
+
+ for child in transform_list.get_children():
+ if child.get_index() >= i:
+ child.queue_free()
+ while i < transform_count:
+ var t := attribute_ref.get_transform(i)
+ var t_editor := TransformEditor.instantiate()
+ transform_list.add_child(t_editor)
+ # Setup top panel
+ if t is AttributeTransform.TransformMatrix:
+ t_editor.type = "matrix"
+ elif t is AttributeTransform.TransformTranslate:
+ t_editor.type = "translate"
+ elif t is AttributeTransform.TransformRotate:
+ t_editor.type = "rotate"
+ elif t is AttributeTransform.TransformScale:
+ t_editor.type = "scale"
+ elif t is AttributeTransform.TransformSkewX:
+ t_editor.type = "skewX"
+ elif t is AttributeTransform.TransformSkewY:
+ t_editor.type = "skewY"
+ t_editor.transform_label.text = t_editor.type
+ t_editor.transform_icon.texture = icons_dict[t_editor.type]
+ t_editor.more_button.pressed.connect(
+ popup_transform_actions.bind(i, t_editor.more_button))
+ # Setup fields.
+ if t is AttributeTransform.TransformMatrix:
+ var field_x1 := create_mini_number_field(t, i, &"x1")
+ var field_x2 := create_mini_number_field(t, i, &"x2")
+ var field_y1 := create_mini_number_field(t, i, &"y1")
+ var field_y2 := create_mini_number_field(t, i, &"y2")
+ var field_o1 := create_mini_number_field(t, i, &"o1")
+ var field_o2 := create_mini_number_field(t, i, &"o2")
+ t_editor.fields = [field_x1, field_x2, field_y1, field_y2, field_o1, field_o1]\
+ as Array[BetterLineEdit]
+ var transform_fields := HBoxContainer.new()
+ transform_fields.alignment = BoxContainer.ALIGNMENT_CENTER
+ transform_fields.add_child(field_x1)
+ transform_fields.add_child(field_y1)
+ transform_fields.add_child(field_o1)
+ var transform_fields_additional := HBoxContainer.new()
+ transform_fields_additional.alignment = BoxContainer.ALIGNMENT_CENTER
+ transform_fields_additional.add_child(field_x2)
+ transform_fields_additional.add_child(field_y2)
+ transform_fields_additional.add_child(field_o2)
+ t_editor.transform_list.add_child(transform_fields)
+ t_editor.transform_list.add_child(transform_fields_additional)
+ elif t is AttributeTransform.TransformTranslate:
+ var field_x := create_mini_number_field(t, i, &"x")
+ var field_y := create_mini_number_field(t, i, &"y")
+ t_editor.fields = [field_x, field_y] as Array[BetterLineEdit]
+ var transform_fields := HBoxContainer.new()
+ transform_fields.alignment = BoxContainer.ALIGNMENT_CENTER
+ transform_fields.add_child(field_x)
+ transform_fields.add_child(field_y)
+ t_editor.transform_list.add_child(transform_fields)
+ elif t is AttributeTransform.TransformRotate:
+ var field_deg := create_mini_number_field(t, i, &"deg")
+ field_deg.mode = field_deg.Mode.ANGLE
+ var field_x := create_mini_number_field(t, i, &"x")
+ var field_y := create_mini_number_field(t, i, &"y")
+ t_editor.fields = [field_deg, field_x, field_y] as Array[BetterLineEdit]
+ var transform_fields := HBoxContainer.new()
+ transform_fields.alignment = BoxContainer.ALIGNMENT_CENTER
+ transform_fields.add_child(field_deg)
+ transform_fields.add_child(field_x)
+ transform_fields.add_child(field_y)
+ t_editor.transform_list.add_child(transform_fields)
+ elif t is AttributeTransform.TransformScale:
+ var field_x := create_mini_number_field(t, i, &"x")
+ var field_y := create_mini_number_field(t, i, &"y")
+ t_editor.fields = [field_x, field_y] as Array[BetterLineEdit]
+ var transform_fields := HBoxContainer.new()
+ transform_fields.alignment = BoxContainer.ALIGNMENT_CENTER
+ transform_fields.add_child(field_x)
+ transform_fields.add_child(field_y)
+ t_editor.transform_list.add_child(transform_fields)
+ elif t is AttributeTransform.TransformSkewX:
+ var field_x := create_mini_number_field(t, i, &"x")
+ t_editor.fields = [field_x] as Array[BetterLineEdit]
+ var transform_fields := HBoxContainer.new()
+ transform_fields.alignment = BoxContainer.ALIGNMENT_CENTER
+ transform_fields.add_child(field_x)
+ t_editor.transform_list.add_child(transform_fields)
+ elif t is AttributeTransform.TransformSkewY:
+ var field_y := create_mini_number_field(t, i, &"y")
+ t_editor.fields = [field_y] as Array[BetterLineEdit]
+ var transform_fields := HBoxContainer.new()
+ transform_fields.alignment = BoxContainer.ALIGNMENT_CENTER
+ transform_fields.add_child(field_y)
+ t_editor.transform_list.add_child(transform_fields)
+ i += 1
+ # Show the add button if there are no transforms.
+ transform_list.visible = (transform_count != 0)
+ add_button.visible = (transform_count == 0)
+ update_final_transform()
+
+func create_mini_number_field(transform: AttributeTransform.Transform, idx: int,
+property: StringName) -> BetterLineEdit:
+ var field := MiniNumberField.instantiate()
+ field.custom_minimum_size.x = 44
+ field.set_value(transform.get(property))
+ field.tooltip_text = property
+ field.value_changed.connect(update_value.bind(idx, property))
+ return field
+
+
+func update_value(new_value: float, idx: int, property: StringName) -> void:
+ UR.create_action("")
+ UR.add_do_method(attribute_ref.set_transform_property.bind(idx, property, new_value))
+ UR.add_do_method(rebuild)
+ UR.add_undo_method(attribute_ref.set_transform_list.bind(get_transform_list()))
+ UR.add_undo_method(rebuild)
+ UR.commit_action()
+
+func insert_transform(idx: int, transform_type: String) -> void:
+ UR.create_action("")
+ UR.add_do_method(attribute_ref.insert_transform.bind(idx, transform_type))
+ UR.add_do_method(rebuild)
+ UR.add_undo_method(attribute_ref.set_transform_list.bind(get_transform_list()))
+ UR.add_undo_method(rebuild)
+ UR.commit_action()
+
+func delete_transform(idx: int) -> void:
+ UR.create_action("")
+ UR.add_do_method(attribute_ref.delete_transform.bind(idx))
+ UR.add_do_method(rebuild)
+ UR.add_undo_method(attribute_ref.set_transform_list.bind(get_transform_list()))
+ UR.add_undo_method(rebuild)
+ UR.commit_action()
+
+func _on_apply_matrix_pressed() -> void:
+ var final_transform := attribute_ref.get_final_transform()
+ UR.create_action("")
+ UR.add_do_method(attribute_ref.set_transform_list.bind([
+ AttributeTransform.TransformMatrix.new(final_transform.x.x, final_transform.x.y,
+ final_transform.y.x, final_transform.y.y, final_transform.origin.x,
+ final_transform.origin.y)] as Array[AttributeTransform.Transform]))
+ UR.add_do_method(rebuild)
+ UR.add_undo_method(attribute_ref.set_transform_list.bind(get_transform_list()))
+ UR.add_undo_method(rebuild)
+ UR.commit_action()
+
+func update_final_transform() -> void:
+ var final_transform := attribute_ref.get_final_transform()
+ x1_edit.set_value(final_transform[0].x)
+ x2_edit.set_value(final_transform[0].y)
+ y1_edit.set_value(final_transform[1].x)
+ y2_edit.set_value(final_transform[1].y)
+ o1_edit.set_value(final_transform[2].x)
+ o2_edit.set_value(final_transform[2].y)
+
+
+func popup_transform_actions(idx: int, control: Control) -> void:
+ var btn_array: Array[Button] = []
+ btn_array.append(Utils.create_btn(tr(&"#insert_after"),
+ popup_new_transform_context.bind(idx + 1, control), false,
+ load("res://visual/icons/InsertAfter.svg")))
+ btn_array.append(Utils.create_btn(tr(&"#insert_before"),
+ popup_new_transform_context.bind(idx, control), false,
+ load("res://visual/icons/InsertBefore.svg")))
+ btn_array.append(Utils.create_btn(tr(&"#delete"), delete_transform.bind(idx), false,
+ load("res://visual/icons/Delete.svg")))
+
+ var context_popup := ContextPopup.instantiate()
+ add_child(context_popup)
+ context_popup.set_button_array(btn_array, true)
+ Utils.popup_under_rect_center(context_popup, control.get_global_rect(), get_viewport())
+
+func popup_new_transform_context(idx: int, control: Control) -> void:
+ Utils.popup_under_rect_center(add_new_transform_context(idx),
+ control.get_global_rect(), get_viewport())
+
+func add_new_transform_context(idx: int) -> ContextPopupType:
+ var btn_array: Array[Button] = []
+ for transform in ["matrix", "translate", "rotate", "scale", "skewX", "skewY"]:
+ var btn := Utils.create_btn(transform, insert_transform.bind(idx, transform),
+ false, icons_dict[transform])
+ btn.add_theme_font_override(&"font", code_font)
+ btn_array.append(btn)
+
+ var transform_context := ContextPopup.instantiate()
+ add_child(transform_context)
+ transform_context.set_button_array(btn_array, true)
+ return transform_context
+
+func _on_popup_hide() -> void:
+ queue_free()
+
+
+func _input(event: InputEvent) -> void:
+ if event.is_action_pressed(&"redo"):
+ if UR.has_redo():
+ UR.redo()
+ elif event.is_action_pressed(&"undo"):
+ if UR.has_undo():
+ UR.undo()
+
+
+# So I have to rebuild this in its entirety to keep the references safe or something...
+func get_transform_list() -> Array[AttributeTransform.Transform]:
+ var t_list: Array[AttributeTransform.Transform] = []
+ for t in attribute_ref.get_transform_list():
+ if t is AttributeTransform.TransformMatrix:
+ t_list.append(AttributeTransform.TransformMatrix.new(
+ t.x1, t.x2, t.y1, t.y2, t.o1, t.o2))
+ elif t is AttributeTransform.TransformTranslate:
+ t_list.append(AttributeTransform.TransformTranslate.new(t.x, t.y))
+ elif t is AttributeTransform.TransformRotate:
+ t_list.append(AttributeTransform.TransformRotate.new(t.deg, t.x, t.y))
+ elif t is AttributeTransform.TransformScale:
+ t_list.append(AttributeTransform.TransformScale.new(t.x, t.y))
+ elif t is AttributeTransform.TransformSkewX:
+ t_list.append(AttributeTransform.TransformSkewX.new(t.x))
+ elif t is AttributeTransform.TransformSkewY:
+ t_list.append(AttributeTransform.TransformSkewY.new(t.y))
+ return t_list
diff --git a/src/ui_elements/transform_popup.tscn b/src/ui_elements/transform_popup.tscn
new file mode 100644
index 000000000..b4be453c7
--- /dev/null
+++ b/src/ui_elements/transform_popup.tscn
@@ -0,0 +1,117 @@
+[gd_scene load_steps=5 format=3 uid="uid://dyc4so8qdkmmc"]
+
+[ext_resource type="Script" path="res://src/ui_elements/transform_popup.gd" id="1_ye80i"]
+[ext_resource type="PackedScene" uid="uid://dad7fkhmsooc6" path="res://src/ui_elements/number_edit.tscn" id="2_tdse4"]
+[ext_resource type="Texture2D" uid="uid://eif2ioi0mw17" path="res://visual/icons/Plus.svg" id="2_w40hh"]
+[ext_resource type="Texture2D" uid="uid://cqg7ga6y3m0v1" path="res://visual/icons/ApplyMatrix.svg" id="3_eegha"]
+
+[node name="TransformPopup" type="Popup"]
+disable_3d = true
+transparent_bg = true
+size = Vector2i(176, 236)
+visible = true
+script = ExtResource("1_ye80i")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+custom_minimum_size = Vector2(168, 0)
+offset_right = 4.0
+offset_bottom = 4.0
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_top = 4
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 4
+
+[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
+custom_minimum_size = Vector2(0, 160)
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="TransformsBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="TransformList" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/TransformsBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="AddButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/TransformsBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 0
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"TranslucentButton"
+icon = ExtResource("2_w40hh")
+icon_alignment = 1
+
+[node name="Separator" type="HSeparator" parent="PanelContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="FinalMatrix" type="GridContainer" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+columns = 3
+
+[node name="X1" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/FinalMatrix" instance=ExtResource("2_tdse4")]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "x1"
+editable = false
+code_font_tooltip = true
+
+[node name="Y1" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/FinalMatrix" instance=ExtResource("2_tdse4")]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "y1"
+editable = false
+code_font_tooltip = true
+
+[node name="O1" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/FinalMatrix" instance=ExtResource("2_tdse4")]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "o1"
+editable = false
+code_font_tooltip = true
+
+[node name="X2" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/FinalMatrix" instance=ExtResource("2_tdse4")]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "x2"
+editable = false
+code_font_tooltip = true
+
+[node name="Y2" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/FinalMatrix" instance=ExtResource("2_tdse4")]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "y2"
+editable = false
+code_font_tooltip = true
+
+[node name="O2" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/FinalMatrix" instance=ExtResource("2_tdse4")]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "o2"
+editable = false
+code_font_tooltip = true
+
+[node name="ApplyMatrix" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "#apply_matrix"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"TranslucentButton"
+icon = ExtResource("3_eegha")
+
+[connection signal="pressed" from="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/ApplyMatrix" to="." method="_on_apply_matrix_pressed"]
diff --git a/src/ui_elements/unknown_field.gd b/src/ui_elements/unknown_field.gd
new file mode 100644
index 000000000..7e59e67ad
--- /dev/null
+++ b/src/ui_elements/unknown_field.gd
@@ -0,0 +1,36 @@
+## An editor to be tied to an AttributeUnknown.
+## Allows attributes to be edited even if they aren't recognized by GodSVG.
+extends BetterLineEdit
+
+signal focused
+var attribute: AttributeUnknown
+var attribute_name: String
+
+func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+ sync(new_value)
+ if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
+ match update_type:
+ Utils.UpdateType.INTERMEDIATE:
+ attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
+ Utils.UpdateType.FINAL:
+ attribute.set_value(new_value, Attribute.SyncMode.FINAL)
+ _:
+ attribute.set_value(new_value)
+
+
+func _ready() -> void:
+ super()
+ set_value(attribute.get_value())
+ tooltip_text = attribute_name + "\n(" + tr(&"#unknown_tooltip") + ")"
+
+
+func _on_focus_entered() -> void:
+ focused.emit()
+ super()
+
+func _on_text_submitted(new_text: String) -> void:
+ set_value(new_text)
+
+
+func sync(new_value: String) -> void:
+ text = new_value
diff --git a/src/ui_elements/unknown_field.tscn b/src/ui_elements/unknown_field.tscn
new file mode 100644
index 000000000..426f966d3
--- /dev/null
+++ b/src/ui_elements/unknown_field.tscn
@@ -0,0 +1,41 @@
+[gd_scene load_steps=5 format=3 uid="uid://mr2c1hti43k4"]
+
+[ext_resource type="Script" path="res://src/ui_elements/unknown_field.gd" id="1_decfb"]
+[ext_resource type="Texture2D" uid="uid://crx4kcj4o01bs" path="res://visual/icons/SmallQuestionMark.svg" id="1_sd7sr"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_y4uj6"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_atgwj"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="UnknownField" type="LineEdit"]
+custom_minimum_size = Vector2(78, 22)
+offset_right = 48.8125
+offset_bottom = 21.0
+focus_mode = 1
+right_icon = ExtResource("1_sd7sr")
+script = ExtResource("1_decfb")
+hover_stylebox = SubResource("StyleBoxFlat_y4uj6")
+focus_stylebox = SubResource("StyleBoxFlat_atgwj")
+code_font_tooltip = true
+
+[connection signal="text_submitted" from="." to="." method="_on_text_submitted"]
diff --git a/src/ui_parts/DeltaHandle.gd b/src/ui_parts/DeltaHandle.gd
new file mode 100644
index 000000000..7a409f39e
--- /dev/null
+++ b/src/ui_parts/DeltaHandle.gd
@@ -0,0 +1,42 @@
+## A handle that binds to a numeric attribute, relative to two other numeric attributes.
+class_name DeltaHandle extends Handle
+
+# Required.
+var x_attribute: AttributeNumeric
+var y_attribute: AttributeNumeric
+var t_attribute : AttributeTransform
+
+var horizontal: bool
+var d_attribute: AttributeNumeric
+
+func _init(id: PackedInt32Array, xref: AttributeNumeric, yref: AttributeNumeric,
+tref : AttributeTransform, dref: AttributeNumeric, p_horizontal: bool) -> void:
+ tid = id
+ x_attribute = xref
+ y_attribute = yref
+ t_attribute = tref
+ d_attribute = dref
+ horizontal = p_horizontal
+ display_mode = Display.SMALL
+ sync()
+
+func set_pos(new_pos: Vector2, undo_redo := false) -> void:
+ if initial_pos != new_pos and undo_redo:
+ d_attribute.set_num(absf(new_pos.x - x_attribute.get_num() if horizontal else\
+ new_pos.y - y_attribute.get_num()), Attribute.SyncMode.FINAL)
+ else:
+ d_attribute.set_num(absf(new_pos.x - x_attribute.get_num() if horizontal else\
+ new_pos.y - y_attribute.get_num()), Attribute.SyncMode.FINAL if undo_redo\
+ else Attribute.SyncMode.INTERMEDIATE)
+ pos = Vector2(x_attribute.get_num(), y_attribute.get_num())
+ if horizontal:
+ pos += Vector2(d_attribute.get_num(), 0.0)
+ else:
+ pos += Vector2(0.0, d_attribute.get_num())
+
+func sync() -> void:
+ if horizontal:
+ pos = Vector2(x_attribute.get_num() + d_attribute.get_num(), y_attribute.get_num())
+ else:
+ pos = Vector2(x_attribute.get_num(), y_attribute.get_num() + d_attribute.get_num())
+ transform = t_attribute.get_final_transform()
diff --git a/src/ui_parts/Handle.gd b/src/ui_parts/Handle.gd
new file mode 100644
index 000000000..37049aae6
--- /dev/null
+++ b/src/ui_parts/Handle.gd
@@ -0,0 +1,19 @@
+## Base class for handles.
+class_name Handle extends RefCounted
+
+enum Display {BIG, SMALL}
+var display_mode := Display.BIG
+
+var tid := PackedInt32Array()
+var pos: Vector2
+var transform : Transform2D
+var initial_pos: Vector2 # The position of a handle when it started being dragged.
+
+func _init() -> void:
+ pass
+
+func sync() -> void:
+ pass
+
+func set_pos(_new_pos: Vector2, _undo_redo := false) -> void:
+ pass
diff --git a/src/ui_parts/PathHandle.gd b/src/ui_parts/PathHandle.gd
new file mode 100644
index 000000000..c3bc88449
--- /dev/null
+++ b/src/ui_parts/PathHandle.gd
@@ -0,0 +1,59 @@
+## A handle that binds to one or two path parameters.
+class_name PathHandle extends Handle
+
+var path_attribute: AttributePath
+var t_attribute: AttributeTransform
+var command_index: int
+var x_param: StringName
+var y_param: StringName
+
+func _init(id: PackedInt32Array, path_ref: Attribute, t_ref: AttributeTransform,
+command_idx: int, x_name := &"x", y_name := &"y") -> void:
+ path_attribute = path_ref
+ t_attribute = t_ref
+ tid = id
+ command_index = command_idx
+ x_param = x_name
+ y_param = y_name
+ sync()
+
+func set_pos(new_pos: Vector2, undo_redo := false) -> void:
+ var command := path_attribute.get_command(command_index)
+ var new_coords := new_pos - command.start if command.relative else new_pos
+ if undo_redo:
+ if initial_pos != new_pos:
+ path_attribute.set_command_property(command_index, x_param, new_coords.x,
+ Attribute.SyncMode.NO_PROPAGATION)
+ path_attribute.set_command_property(command_index, y_param, new_coords.y,
+ Attribute.SyncMode.FINAL)
+ else:
+ if x_param in command:
+ # Don't emit commands_changed for the X change if there'll be a Y change too.
+ path_attribute.set_command_property(command_index, x_param, new_coords.x,
+ Attribute.SyncMode.NO_PROPAGATION if (y_param in command and\
+ command.get(y_param) != new_coords.y) else Attribute.SyncMode.INTERMEDIATE)
+ pos.x = new_pos.x
+ else:
+ pos.x = command.start.x
+ if y_param in command:
+ if command.get(y_param) != new_coords.y:
+ path_attribute.set_command_property(command_index, y_param, new_coords.y,
+ Attribute.SyncMode.INTERMEDIATE)
+ pos.y = new_pos.y
+ else:
+ pos.y = command.start.y
+
+
+func sync() -> void:
+ var command := path_attribute.get_command(command_index)
+ if x_param in command:
+ var command_x: float = command.get(x_param)
+ pos.x = command.start.x + command_x if command.relative else command_x
+ else:
+ pos.x = command.start.x
+ if y_param in command:
+ var command_y: float = command.get(y_param)
+ pos.y = command.start.y + command_y if command.relative else command_y
+ else:
+ pos.y = command.start.y
+ transform = t_attribute.get_final_transform()
diff --git a/src/ui_parts/XYHandle.gd b/src/ui_parts/XYHandle.gd
new file mode 100644
index 000000000..4aa638273
--- /dev/null
+++ b/src/ui_parts/XYHandle.gd
@@ -0,0 +1,31 @@
+## A handle that binds to two numeric attributes.
+class_name XYHandle extends Handle
+
+var x_attribute: AttributeNumeric
+var y_attribute: AttributeNumeric
+var t_attribute: AttributeTransform
+
+func _init(id: PackedInt32Array, xref: AttributeNumeric, yref: AttributeNumeric,
+tref: AttributeTransform) -> void:
+ tid = id
+ x_attribute = xref
+ y_attribute = yref
+ t_attribute = tref
+ sync()
+
+func set_pos(new_pos: Vector2, undo_redo := false) -> void:
+ if undo_redo:
+ if initial_pos != new_pos:
+ x_attribute.set_num(new_pos.x, Attribute.SyncMode.NO_PROPAGATION)
+ y_attribute.set_num(new_pos.y, Attribute.SyncMode.FINAL)
+ else:
+ if new_pos.x != pos.x:
+ x_attribute.set_num(new_pos.x, Attribute.SyncMode.INTERMEDIATE if\
+ new_pos.y == pos.y else Attribute.SyncMode.NO_PROPAGATION)
+ if new_pos.y != pos.y:
+ y_attribute.set_num(new_pos.y, Attribute.SyncMode.INTERMEDIATE)
+ pos = new_pos
+
+func sync() -> void:
+ pos = Vector2(x_attribute.get_num(), y_attribute.get_num())
+ transform = t_attribute.get_final_transform()
diff --git a/src/ui_parts/about_menu.gd b/src/ui_parts/about_menu.gd
new file mode 100644
index 000000000..b9c020170
--- /dev/null
+++ b/src/ui_parts/about_menu.gd
@@ -0,0 +1,20 @@
+extends PanelContainer
+
+@onready var project_founder_list: PanelGrid = %ProjectFounder/List
+@onready var authors_list: PanelGrid = %Developers/List
+@onready var version_label: Label = %VersionLabel
+
+func _ready() -> void:
+ version_label.text = "GodSVG " + ProjectSettings.get_setting(
+ &"application/config/version", "Version information unavailable")
+ project_founder_list.items = AppInfo.project_founder_and_manager
+ project_founder_list.setup()
+ authors_list.items = AppInfo.authors
+ authors_list.setup()
+
+func _on_components_pressed() -> void:
+ OS.shell_open("https://github.com/godotengine/godot/blob/master/COPYRIGHT.txt")
+
+
+func _on_close_pressed() -> void:
+ queue_free()
diff --git a/src/ui_parts/about_menu.tscn b/src/ui_parts/about_menu.tscn
new file mode 100644
index 000000000..5d2612e31
--- /dev/null
+++ b/src/ui_parts/about_menu.tscn
@@ -0,0 +1,364 @@
+[gd_scene load_steps=10 format=3 uid="uid://mhfp37lr7q4f"]
+
+[ext_resource type="Script" path="res://src/ui_parts/about_menu.gd" id="1_xxltt"]
+[ext_resource type="Texture2D" uid="uid://barsurula6j8n" path="res://visual/icon.svg" id="2_y5kl0"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterTabContainer.gd" id="3_1xgiv"]
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="3_e8i1t"]
+[ext_resource type="FontFile" uid="uid://dc0w4sx0h0fui" path="res://visual/fonts/FontBold.ttf" id="4_n6gp0"]
+[ext_resource type="Texture2D" uid="uid://cgxpm1e3v0i3v" path="res://visual/icons/Link.svg" id="6_hbk78"]
+[ext_resource type="Script" path="res://src/ui_elements/GridDrawingControl.gd" id="7_nvctb"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7tlrv"]
+content_margin_left = 8.0
+content_margin_top = 10.0
+content_margin_right = 8.0
+content_margin_bottom = 10.0
+bg_color = Color(0.005, 0.005, 0.05, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.203922, 0.254902, 0.4, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jtvwe"]
+content_margin_left = 6.0
+content_margin_top = 1.0
+content_margin_right = 6.0
+content_margin_bottom = 1.0
+bg_color = Color(0.196078, 0.196078, 0.301961, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.301961, 0.301961, 0.4, 1)
+
+[node name="AboutMenu" type="PanelContainer"]
+custom_minimum_size = Vector2(512, 408)
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -2.0
+offset_top = -2.0
+offset_right = 2.0
+offset_bottom = 2.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_7tlrv")
+script = ExtResource("1_xxltt")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_constants/separation = 8
+
+[node name="TextureRect" type="TextureRect" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+texture = ExtResource("2_y5kl0")
+expand_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+
+[node name="VersionLabel" type="Label" parent="VBoxContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+
+[node name="CopyLabel" type="Label" parent="VBoxContainer/HBoxContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/line_spacing = 0
+theme_override_font_sizes/font_size = 12
+text = "Ā© 2023 MewPurPur
+Ā© 2023-present GodSVG contributors"
+
+[node name="TabContainer" type="TabContainer" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 280)
+layout_mode = 2
+current_tab = 2
+script = ExtResource("3_1xgiv")
+
+[node name="#license_tab" type="ScrollContainer" parent="VBoxContainer/TabContainer"]
+visible = false
+custom_minimum_size = Vector2(480, 240)
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="LicenseLabel" type="Label" parent="VBoxContainer/TabContainer/#license_tab"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("3_e8i1t")
+theme_override_font_sizes/font_size = 10
+text = "MIT License
+
+Copyright (c) 2023 MewPurPur
+Copyright (c) 2023-present GodSVG contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the \"Software\"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE."
+
+[node name="#third-party-licenses_tab" type="ScrollContainer" parent="VBoxContainer/TabContainer"]
+visible = false
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/#third-party-licenses_tab"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 20
+alignment = 1
+
+[node name="ThirdParties" type="VBoxContainer" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="Godot" type="VBoxContainer" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/Godot"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("4_n6gp0")
+text = "Godot Engine"
+horizontal_alignment = 1
+
+[node name="License" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/Godot"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("3_e8i1t")
+theme_override_font_sizes/font_size = 12
+text = "License: Expat"
+horizontal_alignment = 1
+
+[node name="Components" type="Button" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/Godot"]
+layout_mode = 2
+size_flags_horizontal = 4
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_override_font_sizes/font_size = 12
+text = "#godot_third_party"
+icon = ExtResource("6_hbk78")
+
+[node name="NotoSans" type="VBoxContainer" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties"]
+layout_mode = 2
+theme_override_constants/separation = 3
+
+[node name="Label" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/NotoSans"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("4_n6gp0")
+text = "Noto Sans font"
+horizontal_alignment = 1
+
+[node name="Copy" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/NotoSans"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 12
+text = "Ā© 2012, Google Inc."
+horizontal_alignment = 1
+
+[node name="Files" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/NotoSans"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("3_e8i1t")
+theme_override_font_sizes/font_size = 12
+text = "res://visual/fonts/Font.ttf
+res://visual/fonts/FontBold.ttf"
+horizontal_alignment = 1
+
+[node name="License" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/NotoSans"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("3_e8i1t")
+theme_override_font_sizes/font_size = 12
+text = "License: OFL-1.1"
+horizontal_alignment = 1
+
+[node name="JetbrainsMono" type="VBoxContainer" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties"]
+layout_mode = 2
+theme_override_constants/separation = 3
+
+[node name="Label" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/JetbrainsMono"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("4_n6gp0")
+text = "JetBrains Mono font"
+horizontal_alignment = 1
+
+[node name="Copy" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/JetbrainsMono"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 12
+text = "Ā© 2020, JetBrains s.r.o"
+horizontal_alignment = 1
+
+[node name="File" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/JetbrainsMono"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("3_e8i1t")
+theme_override_font_sizes/font_size = 12
+text = "res://visual/fonts/FontMono.ttf"
+horizontal_alignment = 1
+
+[node name="License" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/JetbrainsMono"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("3_e8i1t")
+theme_override_font_sizes/font_size = 12
+text = "License: OFL-1.1"
+horizontal_alignment = 1
+
+[node name="LicenseTexts" type="VBoxContainer" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="Expat" type="VBoxContainer" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/LicenseTexts"]
+layout_mode = 2
+theme_override_constants/separation = 5
+
+[node name="Title" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/LicenseTexts/Expat"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("4_n6gp0")
+text = "Expat license text"
+horizontal_alignment = 1
+
+[node name="Text" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/LicenseTexts/Expat"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("3_e8i1t")
+theme_override_font_sizes/font_size = 10
+text = "Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the \"Software\"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE."
+
+[node name="OFL" type="VBoxContainer" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/LicenseTexts"]
+layout_mode = 2
+theme_override_constants/separation = 5
+
+[node name="Title" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/LicenseTexts/OFL"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("4_n6gp0")
+text = "OFL-1.1 license text"
+horizontal_alignment = 1
+
+[node name="Text" type="Label" parent="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/LicenseTexts/OFL"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("3_e8i1t")
+theme_override_font_sizes/font_size = 10
+text = "PREAMBLE
+
+The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+
+\"Font Software\" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
+
+\"Reserved Font Name\" refers to any names specified as such after the copyright statement(s).
+
+\"Original Version\" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
+
+\"Modified Version\" refers to any derivative made by adding to, deleting, or substituting ā in part or in whole ā any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
+
+\"Author\" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
+
+ 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
+ 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
+ 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
+ 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
+
+TERMINATION
+
+This license becomes null and void if any of the above conditions are not met.
+
+DISCLAIMER
+
+THE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE."
+autowrap_mode = 3
+
+[node name="#authors_tab" type="ScrollContainer" parent="VBoxContainer/TabContainer"]
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/#authors_tab"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 8
+
+[node name="ProjectFounder" type="VBoxContainer" parent="VBoxContainer/TabContainer/#authors_tab/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 6
+
+[node name="Label" type="Label" parent="VBoxContainer/TabContainer/#authors_tab/VBoxContainer/ProjectFounder"]
+layout_mode = 2
+text = "Project Founder and Manager"
+horizontal_alignment = 1
+
+[node name="List" type="GridContainer" parent="VBoxContainer/TabContainer/#authors_tab/VBoxContainer/ProjectFounder"]
+layout_mode = 2
+theme_override_constants/h_separation = -1
+theme_override_constants/v_separation = -1
+script = ExtResource("7_nvctb")
+stylebox = SubResource("StyleBoxFlat_jtvwe")
+
+[node name="Developers" type="VBoxContainer" parent="VBoxContainer/TabContainer/#authors_tab/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 6
+
+[node name="Label" type="Label" parent="VBoxContainer/TabContainer/#authors_tab/VBoxContainer/Developers"]
+layout_mode = 2
+text = "Developers"
+horizontal_alignment = 1
+
+[node name="List" type="GridContainer" parent="VBoxContainer/TabContainer/#authors_tab/VBoxContainer/Developers"]
+layout_mode = 2
+theme_override_constants/h_separation = -1
+theme_override_constants/v_separation = -1
+columns = 2
+script = ExtResource("7_nvctb")
+stylebox = SubResource("StyleBoxFlat_jtvwe")
+
+[node name="Close" type="Button" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 8
+focus_mode = 0
+mouse_default_cursor_shape = 2
+text = "Close"
+
+[connection signal="pressed" from="VBoxContainer/TabContainer/#third-party-licenses_tab/VBoxContainer/ThirdParties/Godot/Components" to="." method="_on_components_pressed"]
+[connection signal="pressed" from="VBoxContainer/Close" to="." method="_on_close_pressed"]
diff --git a/src/ui_parts/alert_dialog.gd b/src/ui_parts/alert_dialog.gd
new file mode 100644
index 000000000..33fd5058d
--- /dev/null
+++ b/src/ui_parts/alert_dialog.gd
@@ -0,0 +1,16 @@
+extends PanelContainer
+
+@onready var title: Label = %MainContainer/TextContainer/Title
+@onready var label: RichTextLabel = %MainContainer/TextContainer/Label
+@onready var ok_button: Button = %MainContainer/OKButton
+
+func _ready() -> void:
+ ok_button.grab_focus()
+
+func setup(message: String, title_text := "#alert", min_width := 180.0) -> void:
+ label.text = tr(message)
+ title.text = tr(title_text)
+ label.custom_minimum_size.x = min_width
+
+func _on_ok_button_pressed() -> void:
+ queue_free()
diff --git a/src/ui_parts/alert_dialog.tscn b/src/ui_parts/alert_dialog.tscn
new file mode 100644
index 000000000..c165a802f
--- /dev/null
+++ b/src/ui_parts/alert_dialog.tscn
@@ -0,0 +1,54 @@
+[gd_scene load_steps=3 format=3 uid="uid://c0x44loihhyyo"]
+
+[ext_resource type="FontFile" uid="uid://dc0w4sx0h0fui" path="res://visual/fonts/FontBold.ttf" id="1_3yrpq"]
+[ext_resource type="Script" path="res://src/ui_parts/alert_dialog.gd" id="1_qntyo"]
+
+[node name="AlertDialog" type="PanelContainer"]
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -2.0
+offset_top = -2.0
+offset_right = 2.0
+offset_bottom = 2.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_qntyo")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 6
+theme_override_constants/margin_right = 8
+theme_override_constants/margin_bottom = 8
+
+[node name="MainContainer" type="VBoxContainer" parent="MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 10
+
+[node name="TextContainer" type="VBoxContainer" parent="MarginContainer/MainContainer"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="MarginContainer/MainContainer/TextContainer"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("1_3yrpq")
+theme_override_font_sizes/font_size = 16
+text = "Alert!"
+horizontal_alignment = 1
+
+[node name="Label" type="RichTextLabel" parent="MarginContainer/MainContainer/TextContainer"]
+custom_minimum_size = Vector2(180, 0)
+layout_mode = 2
+theme_override_font_sizes/normal_font_size = 12
+fit_content = true
+
+[node name="OKButton" type="Button" parent="MarginContainer/MainContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+mouse_default_cursor_shape = 2
+text = "#ok"
+
+[connection signal="pressed" from="MarginContainer/MainContainer/OKButton" to="." method="_on_ok_button_pressed"]
diff --git a/src/ui_parts/autoformat_menu.gd b/src/ui_parts/autoformat_menu.gd
new file mode 100644
index 000000000..6dd0db9b6
--- /dev/null
+++ b/src/ui_parts/autoformat_menu.gd
@@ -0,0 +1,41 @@
+extends PanelContainer
+
+const SettingCheckBox = preload("res://src/ui_elements/setting_check_box.gd")
+
+@onready var number_vbox: VBoxContainer = %NumberVBox
+@onready var color_vbox: VBoxContainer = %ColorVBox
+@onready var path_vbox: VBoxContainer = %PathVBox
+@onready var transform_vbox: VBoxContainer = %TransformVBox
+
+func _ready() -> void:
+ disable_checkboxes()
+
+func _on_cancel_button_pressed() -> void:
+ queue_free()
+
+func _on_autoformat_settings_changed() -> void:
+ SVG.root_tag.replace_self(SVG.root_tag.create_duplicate())
+ disable_checkboxes()
+
+func disable_checkboxes() -> void:
+ for checkbox in number_vbox.get_children():
+ if checkbox is SettingCheckBox:
+ if checkbox.setting_name != "number_enable_autoformatting":
+ set_checkbox_enabled(checkbox, GlobalSettings.number_enable_autoformatting)
+ for checkbox in color_vbox.get_children():
+ if checkbox is SettingCheckBox:
+ if checkbox.setting_name != "color_enable_autoformatting":
+ set_checkbox_enabled(checkbox, GlobalSettings.color_enable_autoformatting)
+ for checkbox in path_vbox.get_children():
+ if checkbox is SettingCheckBox:
+ if checkbox.setting_name != "path_enable_autoformatting":
+ set_checkbox_enabled(checkbox, GlobalSettings.path_enable_autoformatting)
+ for checkbox in transform_vbox.get_children():
+ if checkbox is SettingCheckBox:
+ if checkbox.setting_name != "transform_enable_autoformatting":
+ set_checkbox_enabled(checkbox, GlobalSettings.transform_enable_autoformatting)
+
+func set_checkbox_enabled(checkbox: SettingCheckBox, enabled: bool) -> void:
+ checkbox.disabled = not enabled
+ checkbox.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if enabled else\
+ Control.CURSOR_ARROW
diff --git a/src/ui_parts/autoformat_menu.tscn b/src/ui_parts/autoformat_menu.tscn
new file mode 100644
index 000000000..95b9d3144
--- /dev/null
+++ b/src/ui_parts/autoformat_menu.tscn
@@ -0,0 +1,243 @@
+[gd_scene load_steps=5 format=3 uid="uid://4ve0pwcpch2e"]
+
+[ext_resource type="Script" path="res://src/ui_parts/autoformat_menu.gd" id="1_1463h"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterTabContainer.gd" id="2_p41oe"]
+[ext_resource type="PackedScene" path="res://src/ui_elements/setting_check_box.tscn" id="4_ic3rj"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p36ah"]
+content_margin_left = 8.0
+content_margin_top = 10.0
+content_margin_right = 8.0
+content_margin_bottom = 10.0
+bg_color = Color(0.005, 0.005, 0.05, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.203922, 0.254902, 0.4, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[node name="AutoformatMenu" type="PanelContainer"]
+custom_minimum_size = Vector2(440, 260)
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -2.0
+offset_top = -2.0
+offset_right = 2.0
+offset_bottom = 2.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_p36ah")
+script = ExtResource("1_1463h")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="BetterTabContainer" type="TabContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("2_p41oe")
+
+[node name="XML" type="ScrollContainer" parent="VBoxContainer/BetterTabContainer"]
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="XmlVBox" type="VBoxContainer" parent="VBoxContainer/BetterTabContainer/XML"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 0
+
+[node name="AddTrailingNewline" parent="VBoxContainer/BetterTabContainer/XML/XmlVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#add_trailing_newline"
+section_name = "autoformat"
+setting_name = "xml_add_trailing_newline"
+
+[node name="UseShorthandTagSyntax" parent="VBoxContainer/BetterTabContainer/XML/XmlVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#use_shorthand_tag_syntax"
+section_name = "autoformat"
+setting_name = "xml_shorthand_tags"
+
+[node name="#numbers" type="ScrollContainer" parent="VBoxContainer/BetterTabContainer"]
+visible = false
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="NumberVBox" type="VBoxContainer" parent="VBoxContainer/BetterTabContainer/#numbers"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 0
+
+[node name="NumberEnable" parent="VBoxContainer/BetterTabContainer/#numbers/NumberVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#enable_autoformatting"
+section_name = "autoformat"
+setting_name = "number_enable_autoformatting"
+
+[node name="RemoveZeroPadding" parent="VBoxContainer/BetterTabContainer/#numbers/NumberVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#remove_zero_padding"
+section_name = "autoformat"
+setting_name = "number_remove_zero_padding"
+
+[node name="RemoveLeadingZero" parent="VBoxContainer/BetterTabContainer/#numbers/NumberVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#remove_leading_zero"
+section_name = "autoformat"
+setting_name = "number_remove_leading_zero"
+
+[node name="#colors" type="ScrollContainer" parent="VBoxContainer/BetterTabContainer"]
+visible = false
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="ColorVBox" type="VBoxContainer" parent="VBoxContainer/BetterTabContainer/#colors"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 0
+
+[node name="ColorEnable" parent="VBoxContainer/BetterTabContainer/#colors/ColorVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#enable_autoformatting"
+section_name = "autoformat"
+setting_name = "color_enable_autoformatting"
+
+[node name="ConvertToHex" parent="VBoxContainer/BetterTabContainer/#colors/ColorVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#convert_rgb_to_hex"
+section_name = "autoformat"
+setting_name = "color_convert_rgb_to_hex"
+
+[node name="ConvertToHex2" parent="VBoxContainer/BetterTabContainer/#colors/ColorVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#convert_named_to_hex"
+section_name = "autoformat"
+setting_name = "color_convert_named_to_hex"
+
+[node name="UseShorthandHex" parent="VBoxContainer/BetterTabContainer/#colors/ColorVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#use_shorthand_hex_code"
+section_name = "autoformat"
+setting_name = "color_use_shorthand_hex_code"
+
+[node name="UseNamedColors" parent="VBoxContainer/BetterTabContainer/#colors/ColorVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#use_short_named_colors"
+section_name = "autoformat"
+setting_name = "color_use_short_named_colors"
+
+[node name="#paths" type="ScrollContainer" parent="VBoxContainer/BetterTabContainer"]
+visible = false
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="PathVBox" type="VBoxContainer" parent="VBoxContainer/BetterTabContainer/#paths"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 0
+
+[node name="PathEnable" parent="VBoxContainer/BetterTabContainer/#paths/PathVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#enable_autoformatting"
+section_name = "autoformat"
+setting_name = "path_enable_autoformatting"
+
+[node name="CompressNumbers" parent="VBoxContainer/BetterTabContainer/#paths/PathVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#compress_numbers"
+section_name = "autoformat"
+setting_name = "path_compress_numbers"
+
+[node name="MinimizeSpacing" parent="VBoxContainer/BetterTabContainer/#paths/PathVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#minimize_spacing"
+section_name = "autoformat"
+setting_name = "path_minimize_spacing"
+
+[node name="RemoveSpacingAfterFlags" parent="VBoxContainer/BetterTabContainer/#paths/PathVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#remove_spacing_after_flags"
+section_name = "autoformat"
+setting_name = "path_remove_spacing_after_flags"
+
+[node name="RemoveConsecutiveCommands" parent="VBoxContainer/BetterTabContainer/#paths/PathVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#remove_consecutive_commands"
+section_name = "autoformat"
+setting_name = "path_remove_consecutive_commands"
+
+[node name="#transforms" type="ScrollContainer" parent="VBoxContainer/BetterTabContainer"]
+visible = false
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="TransformVBox" type="VBoxContainer" parent="VBoxContainer/BetterTabContainer/#transforms"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 0
+
+[node name="TransformEnable" parent="VBoxContainer/BetterTabContainer/#transforms/TransformVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#enable_autoformatting"
+section_name = "autoformat"
+setting_name = "transform_enable_autoformatting"
+
+[node name="CompressNumbers" parent="VBoxContainer/BetterTabContainer/#transforms/TransformVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#compress_numbers"
+section_name = "autoformat"
+setting_name = "transform_compress_numbers"
+
+[node name="MinimizeSpacing" parent="VBoxContainer/BetterTabContainer/#transforms/TransformVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#minimize_spacing"
+section_name = "autoformat"
+setting_name = "transform_minimize_spacing"
+
+[node name="RemoveUnnecessaryParameters" parent="VBoxContainer/BetterTabContainer/#transforms/TransformVBox" instance=ExtResource("4_ic3rj")]
+layout_mode = 2
+text = "#remove_unnecessary_parameters"
+section_name = "autoformat"
+setting_name = "transform_remove_unnecessary_params"
+
+[node name="CancelButton" type="Button" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 8
+focus_mode = 0
+mouse_default_cursor_shape = 2
+text = "#close"
+
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/XML/XmlVBox/AddTrailingNewline" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/XML/XmlVBox/UseShorthandTagSyntax" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#numbers/NumberVBox/NumberEnable" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#numbers/NumberVBox/RemoveZeroPadding" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#numbers/NumberVBox/RemoveLeadingZero" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#colors/ColorVBox/ColorEnable" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#colors/ColorVBox/ConvertToHex" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#colors/ColorVBox/ConvertToHex2" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#colors/ColorVBox/UseShorthandHex" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#colors/ColorVBox/UseNamedColors" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#paths/PathVBox/PathEnable" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#paths/PathVBox/CompressNumbers" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#paths/PathVBox/MinimizeSpacing" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#paths/PathVBox/RemoveSpacingAfterFlags" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#paths/PathVBox/RemoveConsecutiveCommands" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#transforms/TransformVBox/TransformEnable" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#transforms/TransformVBox/CompressNumbers" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#transforms/TransformVBox/MinimizeSpacing" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/BetterTabContainer/#transforms/TransformVBox/RemoveUnnecessaryParameters" to="." method="_on_autoformat_settings_changed"]
+[connection signal="pressed" from="VBoxContainer/CancelButton" to="." method="_on_cancel_button_pressed"]
diff --git a/src/ui_parts/code_editor.gd b/src/ui_parts/code_editor.gd
new file mode 100644
index 000000000..a4acff221
--- /dev/null
+++ b/src/ui_parts/code_editor.gd
@@ -0,0 +1,128 @@
+extends VBoxContainer
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const autoformat_menu = preload("res://src/ui_parts/autoformat_menu.tscn")
+
+@onready var code_edit: TextEdit = $ScriptEditor/SVGCodeEdit
+@onready var error_bar: PanelContainer = $ScriptEditor/ErrorBar
+@onready var error_label: RichTextLabel = $ScriptEditor/ErrorBar/Label
+@onready var size_label: Label = %SizeLabelContainer/SizeLabel
+@onready var size_label_container: PanelContainer = %SizeLabelContainer
+@onready var file_button: Button = %FileButton
+
+func _ready() -> void:
+ SVG.parsing_finished.connect(update_error)
+ auto_update_text()
+ update_size_label()
+ update_file_button()
+ setup_theme(false)
+ code_edit.clear_undo_history()
+ SVG.root_tag.attribute_changed.connect(auto_update_text.unbind(1))
+ SVG.root_tag.child_attribute_changed.connect(auto_update_text.unbind(1))
+ SVG.root_tag.tag_layout_changed.connect(auto_update_text)
+ SVG.root_tag.changed_unknown.connect(auto_update_text)
+ GlobalSettings.save_data.current_file_path_changed.connect(update_file_button)
+
+func auto_update_text() -> void:
+ if not code_edit.has_focus():
+ code_edit.text = SVG.text
+ code_edit.clear_undo_history()
+ update_size_label()
+
+func update_error(err_id: SVGParser.ParseError) -> void:
+ if err_id == SVGParser.ParseError.OK:
+ if error_bar.visible:
+ error_bar.hide()
+ var error_bar_real_height := error_bar.size.y - 2
+ code_edit.custom_minimum_size.y += error_bar_real_height
+ code_edit.size.y += error_bar_real_height
+ setup_theme(false)
+ else:
+ # When the error is shown, the code editor's theme is changed to match up.
+ if not error_bar.visible:
+ error_bar.show()
+ error_label.text = tr(SVGParser.get_error_stringname(err_id))
+ var error_bar_real_height := error_bar.size.y - 2
+ code_edit.custom_minimum_size.y -= error_bar_real_height
+ code_edit.size.y -= error_bar_real_height
+ setup_theme(true)
+
+func setup_theme(match_below: bool) -> void:
+ code_edit.begin_bulk_theme_override()
+ for theming in [&"normal", &"focus"]:
+ var stylebox := ThemeDB.get_project_theme().\
+ get_stylebox(theming, &"TextEdit").duplicate()
+ stylebox.corner_radius_top_right = 0
+ stylebox.corner_radius_top_left = 0
+ stylebox.border_width_top = 2
+ if match_below:
+ stylebox.corner_radius_bottom_right = 0
+ stylebox.corner_radius_bottom_left = 0
+ stylebox.border_width_bottom = 1
+ code_edit.add_theme_stylebox_override(theming, stylebox)
+ code_edit.end_bulk_theme_override()
+
+
+func _on_copy_button_pressed() -> void:
+ DisplayServer.clipboard_set(code_edit.text)
+
+
+func _on_import_button_pressed() -> void:
+ SVG.open_import_dialog()
+
+func _on_export_button_pressed() -> void:
+ SVG.open_export_dialog()
+
+func set_new_text(svg_text: String) -> void:
+ code_edit.text = svg_text
+ _on_code_edit_text_changed() # Call it automatically yeah.
+
+
+func _on_code_edit_text_changed() -> void:
+ SVG.text = code_edit.text
+ SVG.update_tags()
+
+
+func _input(event: InputEvent) -> void:
+ if (code_edit.has_focus() and event is InputEventMouseButton and\
+ not code_edit.get_global_rect().has_point(event.position)):
+ code_edit.release_focus()
+
+
+func update_size_label() -> void:
+ var svg_text_size := SVG.text.length()
+ size_label.text = String.humanize_size(svg_text_size)
+ size_label_container.tooltip_text = String.num_uint64(svg_text_size) + " B"
+
+func update_file_button() -> void:
+ var file_path := GlobalSettings.save_data.current_file_path
+ file_button.visible = !file_path.is_empty()
+ file_button.text = file_path.get_file()
+ file_button.tooltip_text = file_path.get_file()
+ Utils.set_max_text_width(file_button, 120.0, 12.0)
+ if not file_path.is_empty():
+ get_window().title = file_path.get_file() + " - GodSVG"
+ else:
+ get_window().title = "GodSVG"
+
+func _on_svg_code_edit_focus_exited() -> void:
+ code_edit.text = SVG.text
+ if GlobalSettings.save_data.svg_text != code_edit.text:
+ SVG.update_text(true)
+
+
+func _on_autoformat_button_pressed() -> void:
+ var autoformat_menu_instance := autoformat_menu.instantiate()
+ HandlerGUI.add_overlay(autoformat_menu_instance)
+
+func _on_file_button_pressed() -> void:
+ var btn_array: Array[Button] = [Utils.create_btn(tr(&"#remove_association"),
+ clear_file_path)]
+ var context_popup := ContextPopup.instantiate()
+ add_child(context_popup)
+ context_popup.set_button_array(btn_array, false, file_button.size.x)
+ Utils.popup_under_rect_center(context_popup, file_button.get_global_rect(),
+ get_viewport())
+
+func clear_file_path() -> void:
+ GlobalSettings.modify_save_data(&"current_file_path", "")
diff --git a/src/ui_parts/code_editor.tscn b/src/ui_parts/code_editor.tscn
new file mode 100644
index 000000000..28ef48322
--- /dev/null
+++ b/src/ui_parts/code_editor.tscn
@@ -0,0 +1,176 @@
+[gd_scene load_steps=14 format=3 uid="uid://cr1fdlmbknnko"]
+
+[ext_resource type="Script" path="res://src/ui_parts/code_editor.gd" id="1_nffk0"]
+[ext_resource type="Texture2D" uid="uid://ckkkgof1hcbld" path="res://visual/icons/Gear.svg" id="2_k8onw"]
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="2_p4nol"]
+[ext_resource type="Texture2D" uid="uid://ccvjkdd0s7rb4" path="res://visual/icons/Copy.svg" id="3_6x47p"]
+[ext_resource type="Texture2D" uid="uid://6ymbl3jqersp" path="res://visual/icons/Import.svg" id="4_cuhac"]
+[ext_resource type="Texture2D" uid="uid://d0uvwj0t44n6v" path="res://visual/icons/Export.svg" id="5_pgurh"]
+[ext_resource type="Script" path="res://src/parsers/SVGHighlighter.gd" id="6_hpsqx"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterTextEdit.gd" id="8_ser4i"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q56qh"]
+content_margin_left = 8.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 1.0
+bg_color = Color(0.0975, 0.0975, 0.15, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_color = Color(0.152941, 0.152941, 0.2, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rppt3"]
+content_margin_left = 5.0
+content_margin_top = 2.0
+content_margin_right = 5.0
+content_margin_bottom = 2.0
+bg_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_njere"]
+script = ExtResource("6_hpsqx")
+symbol_color = Color(0.670588, 0.788235, 1, 1)
+tag_color = Color(1, 0.54902, 0.8, 1)
+attribute_color = Color(0.737255, 0.878431, 1, 1)
+string_color = Color(0.631373, 1, 0.878431, 1)
+comment_color = Color(0.803922, 0.811765, 0.823529, 0.501961)
+text_color = Color(0.803922, 0.811765, 0.917647, 0.67451)
+error_color = Color(1, 0.52549, 0.419608, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1i105"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k0een"]
+content_margin_left = 10.0
+content_margin_right = 8.0
+bg_color = Color(0.0975, 0.0975, 0.15, 1)
+border_width_left = 2
+border_width_top = 1
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.1875, 0.1875, 0.25, 1)
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="CodeEditor" type="VBoxContainer"]
+size_flags_vertical = 3
+theme_override_constants/separation = 0
+script = ExtResource("1_nffk0")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_q56qh")
+
+[node name="CodeButtons" type="HBoxContainer" parent="PanelContainer"]
+layout_mode = 2
+
+[node name="AutoformatButton" type="Button" parent="PanelContainer/CodeButtons"]
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("2_k8onw")
+
+[node name="SizeLabelContainer" type="PanelContainer" parent="PanelContainer/CodeButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+theme_override_styles/panel = SubResource("StyleBoxFlat_rppt3")
+
+[node name="SizeLabel" type="Label" parent="PanelContainer/CodeButtons/SizeLabelContainer"]
+layout_mode = 2
+theme_override_fonts/font = ExtResource("2_p4nol")
+theme_override_font_sizes/font_size = 12
+
+[node name="MetaActions" type="HBoxContainer" parent="PanelContainer/CodeButtons"]
+layout_mode = 2
+size_flags_horizontal = 10
+
+[node name="FileButton" type="Button" parent="PanelContainer/CodeButtons/MetaActions"]
+unique_name_in_owner = true
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"FlatButton"
+text_overrun_behavior = 3
+
+[node name="Control" type="Control" parent="PanelContainer/CodeButtons/MetaActions"]
+layout_mode = 2
+
+[node name="CopyButton" type="Button" parent="PanelContainer/CodeButtons/MetaActions"]
+layout_mode = 2
+tooltip_text = "#copy_button_tooltip"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("3_6x47p")
+
+[node name="ImportButton" type="Button" parent="PanelContainer/CodeButtons/MetaActions"]
+layout_mode = 2
+tooltip_text = "#import_button_tooltip"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("4_cuhac")
+
+[node name="ExportButton" type="Button" parent="PanelContainer/CodeButtons/MetaActions"]
+layout_mode = 2
+tooltip_text = "#export_button_tooltip"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("5_pgurh")
+
+[node name="ScriptEditor" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = -2
+
+[node name="SVGCodeEdit" type="TextEdit" parent="ScriptEditor"]
+custom_minimum_size = Vector2(0, 96)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+syntax_highlighter = SubResource("SyntaxHighlighter_njere")
+highlight_all_occurrences = true
+script = ExtResource("8_ser4i")
+hover_stylebox = SubResource("StyleBoxFlat_1i105")
+block_non_ascii = true
+
+[node name="ErrorBar" type="PanelContainer" parent="ScriptEditor"]
+visible = false
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_k0een")
+
+[node name="Label" type="RichTextLabel" parent="ScriptEditor/ErrorBar"]
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+theme_override_colors/default_color = Color(1, 0.4, 0.4, 1)
+theme_override_fonts/normal_font = ExtResource("2_p4nol")
+theme_override_font_sizes/normal_font_size = 14
+fit_content = true
+
+[connection signal="pressed" from="PanelContainer/CodeButtons/AutoformatButton" to="." method="_on_autoformat_button_pressed"]
+[connection signal="pressed" from="PanelContainer/CodeButtons/MetaActions/FileButton" to="." method="_on_file_button_pressed"]
+[connection signal="pressed" from="PanelContainer/CodeButtons/MetaActions/CopyButton" to="." method="_on_copy_button_pressed"]
+[connection signal="pressed" from="PanelContainer/CodeButtons/MetaActions/ImportButton" to="." method="_on_import_button_pressed"]
+[connection signal="pressed" from="PanelContainer/CodeButtons/MetaActions/ExportButton" to="." method="_on_export_button_pressed"]
+[connection signal="caret_changed" from="ScriptEditor/SVGCodeEdit" to="ScriptEditor/SVGCodeEdit" method="redraw_caret"]
+[connection signal="focus_entered" from="ScriptEditor/SVGCodeEdit" to="ScriptEditor/SVGCodeEdit" method="_on_focus_entered"]
+[connection signal="focus_exited" from="ScriptEditor/SVGCodeEdit" to="." method="_on_svg_code_edit_focus_exited"]
+[connection signal="focus_exited" from="ScriptEditor/SVGCodeEdit" to="ScriptEditor/SVGCodeEdit" method="_on_focus_exited"]
+[connection signal="text_changed" from="ScriptEditor/SVGCodeEdit" to="." method="_on_code_edit_text_changed"]
diff --git a/src/ui_parts/configure_color_popup.gd b/src/ui_parts/configure_color_popup.gd
new file mode 100644
index 000000000..ce12ea4b1
--- /dev/null
+++ b/src/ui_parts/configure_color_popup.gd
@@ -0,0 +1,53 @@
+extends Popup
+
+signal color_deletion_requested
+
+@onready var color_label: Label = %ConfigureContainer/TopContainer/ColorLabel
+@onready var color_name_edit: BetterLineEdit = %ConfigureContainer/TopContainer/NameEdit
+@onready var color_name_edit_button: Button = %ConfigureContainer/TopContainer/EditButton
+@onready var color_edit: HBoxContainer = %ConfigureContainer/BottomContainer/ColorEdit
+@onready var delete_button: Button = %ConfigureContainer/BottomContainer/DeleteButton
+
+var named_color: NamedColor
+
+func _ready() -> void:
+ set_label_text(named_color.name)
+ color_edit.value = named_color.color
+
+
+func _on_edit_button_pressed() -> void:
+ color_name_edit.text = named_color.name
+ color_name_edit.show()
+ color_name_edit.grab_focus()
+ color_name_edit.caret_column = color_name_edit.text.length()
+ color_label.hide()
+ color_name_edit_button.hide()
+
+
+func _on_name_edit_text_submitted(new_text: String) -> void:
+ var new_name := new_text.strip_edges()
+ set_label_text(new_name)
+ hide_name_edit()
+
+
+func hide_name_edit() -> void:
+ color_name_edit.hide()
+ color_name_edit_button.show()
+ color_label.show()
+
+func set_label_text(new_text: String) -> void:
+ if new_text.is_empty():
+ color_label.text = tr(&"#unnamed")
+ color_label.add_theme_color_override(&"font_color", Color(0.5, 0.5, 0.5))
+ else:
+ color_label.text = new_text
+ color_label.remove_theme_color_override(&"font_color")
+
+
+func _on_delete_button_pressed() -> void:
+ color_deletion_requested.emit()
+ queue_free()
+
+
+func _on_popup_hide() -> void:
+ queue_free()
diff --git a/src/ui_parts/configure_color_popup.tscn b/src/ui_parts/configure_color_popup.tscn
new file mode 100644
index 000000000..a59fad03f
--- /dev/null
+++ b/src/ui_parts/configure_color_popup.tscn
@@ -0,0 +1,97 @@
+[gd_scene load_steps=8 format=3 uid="uid://b7wobq0ndm35"]
+
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="1_i4wi2"]
+[ext_resource type="Script" path="res://src/ui_parts/configure_color_popup.gd" id="1_x6cll"]
+[ext_resource type="Texture2D" uid="uid://dr2erka82g6j4" path="res://visual/icons/Edit.svg" id="2_0dind"]
+[ext_resource type="FontFile" uid="uid://clpf84p1lfwlp" path="res://visual/fonts/Font.ttf" id="2_j4qfn"]
+[ext_resource type="PackedScene" uid="uid://5f8uxavn1or1" path="res://src/ui_elements/color_edit.tscn" id="3_dq5ly"]
+[ext_resource type="Texture2D" uid="uid://cj5x2ti8150ja" path="res://visual/icons/Delete.svg" id="5_5n8tt"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bicmw"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="ConfigurePopup" type="Popup"]
+transparent_bg = true
+size = Vector2i(128, 70)
+visible = true
+script = ExtResource("1_x6cll")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+custom_minimum_size = Vector2(128, 40)
+offset_right = 4.0
+offset_bottom = 4.0
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 6
+theme_override_constants/margin_top = 4
+theme_override_constants/margin_right = 6
+theme_override_constants/margin_bottom = 6
+
+[node name="ConfigureContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="TopContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/ConfigureContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_constants/separation = 6
+
+[node name="ColorLabel" type="Label" parent="PanelContainer/MarginContainer/ConfigureContainer/TopContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 13
+text = "Yeet"
+horizontal_alignment = 1
+
+[node name="NameEdit" type="LineEdit" parent="PanelContainer/MarginContainer/ConfigureContainer/TopContainer"]
+visible = false
+custom_minimum_size = Vector2(80, 0)
+layout_mode = 2
+focus_mode = 1
+theme_override_fonts/font = ExtResource("2_j4qfn")
+theme_override_styles/normal = SubResource("StyleBoxFlat_bicmw")
+max_length = 30
+script = ExtResource("1_i4wi2")
+
+[node name="EditButton" type="Button" parent="PanelContainer/MarginContainer/ConfigureContainer/TopContainer"]
+layout_mode = 2
+tooltip_text = "#edit_color_name"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"FlatButton"
+icon = ExtResource("2_0dind")
+
+[node name="BottomContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/ConfigureContainer"]
+layout_mode = 2
+theme_override_constants/separation = 6
+alignment = 1
+
+[node name="ColorEdit" parent="PanelContainer/MarginContainer/ConfigureContainer/BottomContainer" instance=ExtResource("3_dq5ly")]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+enable_palettes = false
+
+[node name="DeleteButton" type="Button" parent="PanelContainer/MarginContainer/ConfigureContainer/BottomContainer"]
+layout_mode = 2
+tooltip_text = "#delete_color"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("5_5n8tt")
+
+[connection signal="popup_hide" from="." to="." method="_on_popup_hide"]
+[connection signal="text_submitted" from="PanelContainer/MarginContainer/ConfigureContainer/TopContainer/NameEdit" to="." method="_on_name_edit_text_submitted"]
+[connection signal="pressed" from="PanelContainer/MarginContainer/ConfigureContainer/TopContainer/EditButton" to="." method="_on_edit_button_pressed"]
+[connection signal="pressed" from="PanelContainer/MarginContainer/ConfigureContainer/BottomContainer/DeleteButton" to="." method="_on_delete_button_pressed"]
diff --git a/src/ui_parts/display.gd b/src/ui_parts/display.gd
new file mode 100644
index 000000000..31e5da845
--- /dev/null
+++ b/src/ui_parts/display.gd
@@ -0,0 +1,103 @@
+extends VBoxContainer
+
+const settings_menu = preload("settings_menu.tscn")
+const about_menu = preload("about_menu.tscn")
+const docs = preload("docs.tscn")
+
+const NumberEditType = preload("res://src/ui_elements/number_edit.gd")
+const BetterToggleButtonType = preload("res://src/ui_elements/BetterToggleButton.gd")
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const NumberField = preload("res://src/ui_elements/number_field.tscn")
+
+@onready var viewport: SubViewport = $ViewportPanel/ViewportContainer/Viewport
+@onready var controls: Control = %Checkerboard/Controls
+@onready var grid_visuals: Camera2D = $ViewportPanel/ViewportContainer/Viewport/ViewCamera
+@onready var visuals_button: Button = %LeftMenu/Visuals
+@onready var more_button: Button = %LeftMenu/MoreOptions
+@onready var snapper: NumberEditType = %LeftMenu/Snapping/NumberEdit
+@onready var snap_button: BetterToggleButtonType = %LeftMenu/Snapping/SnapButton
+
+
+func _ready() -> void:
+ update_snap_config()
+
+func update_snap_config() -> void:
+ var snap_config := GlobalSettings.save_data.snap
+ var snap_enabled := snap_config > 0.0
+ snap_button.button_pressed = snap_enabled
+ snapper.editable = snap_enabled
+ snapper.set_value(absf(snap_config))
+
+func _on_settings_pressed() -> void:
+ var settings_menu_instance := settings_menu.instantiate()
+ HandlerGUI.add_overlay(settings_menu_instance)
+
+func _on_visuals_button_pressed() -> void:
+ var btn_arr: Array[Button] = [
+ Utils.create_checkbox(tr(&"#show_grid"), toggle_grid_visuals, grid_visuals.visible),
+ Utils.create_checkbox(tr(&"#show_handles"), toggle_handles_visuals,
+ controls.visible),
+ Utils.create_checkbox(tr(&"#rasterize_svg"), toggle_rasterization,
+ viewport.display_texture.rasterized),
+ ]
+
+ var visuals_popup := ContextPopup.instantiate()
+ add_child(visuals_popup)
+ visuals_popup.set_button_array(btn_arr, true)
+ Utils.popup_under_rect_center(visuals_popup, visuals_button.get_global_rect(),
+ get_viewport())
+
+func _on_more_options_pressed() -> void:
+ var about_btn := Utils.create_btn(tr(&"#about_button_text"), open_about, false,
+ load("res://visual/icon.svg"))
+ about_btn.expand_icon = true
+ var buttons_arr: Array[Button] = [
+ Utils.create_btn(tr(&"#repo_button_text"), open_godsvg_repo, false,
+ load("res://visual/icons/Link.svg")),
+ about_btn,
+ Utils.create_btn(tr(&"#docs_button_text"), open_docs, false,
+ load("res://visual/icons/Docs.svg")),
+ Utils.create_btn(tr(&"#donate_button_text"), open_sponsor, false,
+ load("res://visual/icons/Heart.svg")),
+ ]
+
+ var more_popup := ContextPopup.instantiate()
+ add_child(more_popup)
+ more_popup.set_button_array(buttons_arr, true)
+ Utils.popup_under_rect_center(more_popup, more_button.get_global_rect(),
+ get_viewport())
+
+func open_godsvg_repo() -> void:
+ OS.shell_open("https://github.com/MewPurPur/GodSVG")
+
+func open_about() -> void:
+ var about_menu_instance := about_menu.instantiate()
+ HandlerGUI.add_overlay(about_menu_instance)
+
+func open_docs() -> void:
+ var docs_instance := docs.instantiate()
+ HandlerGUI.add_overlay(docs_instance)
+
+func open_sponsor() -> void:
+ OS.shell_open("https://github.com/sponsors/MewPurPur")
+
+func toggle_grid_visuals() -> void:
+ grid_visuals.visible = not grid_visuals.visible
+
+func toggle_handles_visuals() -> void:
+ controls.visible = not controls.visible
+
+func toggle_rasterization() -> void:
+ viewport.display_texture.rasterized = not viewport.display_texture.rasterized
+
+
+func _on_snap_button_toggled(toggled_on: bool) -> void:
+ GlobalSettings.modify_save_data(&"snap",
+ absf(GlobalSettings.save_data.snap) * (1 if toggled_on else -1))
+ update_snap_config()
+
+func _on_number_edit_value_changed(new_value: float) -> void:
+ GlobalSettings.modify_save_data(&"snap",
+ new_value * signf(GlobalSettings.save_data.snap))
+ update_snap_config()
diff --git a/src/ui_parts/display.tscn b/src/ui_parts/display.tscn
new file mode 100644
index 000000000..137f0ab52
--- /dev/null
+++ b/src/ui_parts/display.tscn
@@ -0,0 +1,209 @@
+[gd_scene load_steps=21 format=3 uid="uid://bvrncl7e6yn5b"]
+
+[ext_resource type="Script" path="res://src/ui_parts/display.gd" id="1_oib5g"]
+[ext_resource type="Texture2D" uid="uid://ccbta5q43jobk" path="res://visual/icons/More.svg" id="2_3wliq"]
+[ext_resource type="Texture2D" uid="uid://ckkkgof1hcbld" path="res://visual/icons/Gear.svg" id="3_0w618"]
+[ext_resource type="Texture2D" uid="uid://kkxyv1gyrjgj" path="res://visual/icons/Visuals.svg" id="4_n3qjt"]
+[ext_resource type="Texture2D" uid="uid://buire51l0mifg" path="res://visual/icons/Snap.svg" id="5_1k2cq"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterToggleButton.gd" id="6_3v3ve"]
+[ext_resource type="PackedScene" uid="uid://dad7fkhmsooc6" path="res://src/ui_elements/number_edit.tscn" id="7_wrrfr"]
+[ext_resource type="PackedScene" uid="uid://oltvrf01xrxl" path="res://src/ui_parts/zoom_menu.tscn" id="8_xtdmn"]
+[ext_resource type="Script" path="res://src/ui_parts/viewport.gd" id="9_4xrk7"]
+[ext_resource type="Shader" path="res://src/shaders/zoom_shader.gdshader" id="10_x7ybk"]
+[ext_resource type="Texture2D" uid="uid://c68og6bsqt0lb" path="res://visual/icons/backgrounds/Checkerboard.svg" id="11_1bm1s"]
+[ext_resource type="Script" path="res://src/ui_parts/display_texture.gd" id="12_qi23s"]
+[ext_resource type="Script" path="res://src/ui_parts/handles_manager.gd" id="13_lwhwy"]
+[ext_resource type="Script" path="res://src/ui_parts/view_camera.gd" id="14_yjb74"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nxvgo"]
+content_margin_left = 7.0
+content_margin_top = 6.0
+content_margin_right = 7.0
+content_margin_bottom = 6.0
+bg_color = Color(0.0235294, 0.0235294, 0.0784314, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_eujxa"]
+bg_color = Color(0.866667, 0.933333, 1, 0.133333)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xe141"]
+draw_center = false
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.0666667)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kqvye"]
+draw_center = false
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jk4ec"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_color = Color(0.329412, 0.403922, 0.54902, 1)
+corner_detail = 1
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_kqplg"]
+shader = ExtResource("10_x7ybk")
+shader_parameter/uv_scale = 1.0
+
+[node name="Display" type="VBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 0
+script = ExtResource("1_oib5g")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+theme_override_styles/panel = SubResource("StyleBoxFlat_nxvgo")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer"]
+layout_mode = 2
+alignment = 2
+
+[node name="LeftMenu" type="HBoxContainer" parent="PanelContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 2
+theme_override_constants/separation = 5
+
+[node name="MoreOptions" type="Button" parent="PanelContainer/HBoxContainer/LeftMenu"]
+layout_mode = 2
+size_flags_horizontal = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("2_3wliq")
+icon_alignment = 1
+
+[node name="Settings" type="Button" parent="PanelContainer/HBoxContainer/LeftMenu"]
+layout_mode = 2
+size_flags_horizontal = 2
+tooltip_text = "#settings"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("3_0w618")
+icon_alignment = 1
+
+[node name="Visuals" type="Button" parent="PanelContainer/HBoxContainer/LeftMenu"]
+layout_mode = 2
+size_flags_horizontal = 2
+tooltip_text = "#visuals"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("4_n3qjt")
+icon_alignment = 1
+
+[node name="Snapping" type="HBoxContainer" parent="PanelContainer/HBoxContainer/LeftMenu"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="SnapButton" type="Button" parent="PanelContainer/HBoxContainer/LeftMenu/Snapping"]
+layout_mode = 2
+tooltip_text = "#enable_snap"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"RightConnectedButton"
+toggle_mode = true
+icon = ExtResource("5_1k2cq")
+script = ExtResource("6_3v3ve")
+hover_pressed_stylebox = SubResource("StyleBoxFlat_eujxa")
+
+[node name="NumberEdit" parent="PanelContainer/HBoxContainer/LeftMenu/Snapping" instance=ExtResource("7_wrrfr")]
+custom_minimum_size = Vector2(46, 22)
+layout_mode = 2
+tooltip_text = "#snap_size"
+theme_type_variation = &"LeftConnectedLineEdit"
+max_length = 20
+editable = false
+min_value = 0.001
+allow_lower = false
+hover_stylebox = SubResource("StyleBoxFlat_xe141")
+focus_stylebox = SubResource("StyleBoxFlat_kqvye")
+
+[node name="ZoomMenu" parent="PanelContainer/HBoxContainer" instance=ExtResource("8_xtdmn")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="ViewportPanel" type="PanelContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_jk4ec")
+
+[node name="ViewportContainer" type="SubViewportContainer" parent="ViewportPanel"]
+process_mode = 3
+custom_minimum_size = Vector2(450, 0)
+layout_mode = 2
+size_flags_vertical = 3
+stretch = true
+
+[node name="Viewport" type="SubViewport" parent="ViewportPanel/ViewportContainer"]
+disable_3d = true
+handle_input_locally = false
+gui_snap_controls_to_pixels = false
+size = Vector2i(1022, 602)
+size_2d_override_stretch = true
+render_target_update_mode = 4
+script = ExtResource("9_4xrk7")
+
+[node name="Checkerboard" type="TextureRect" parent="ViewportPanel/ViewportContainer/Viewport"]
+unique_name_in_owner = true
+texture_filter = 1
+material = SubResource("ShaderMaterial_kqplg")
+texture = ExtResource("11_1bm1s")
+expand_mode = 1
+stretch_mode = 1
+
+[node name="DisplayTexture" type="TextureRect" parent="ViewportPanel/ViewportContainer/Viewport/Checkerboard"]
+clip_contents = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+expand_mode = 1
+script = ExtResource("12_qi23s")
+
+[node name="Controls" type="Control" parent="ViewportPanel/ViewportContainer/Viewport/Checkerboard"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 1
+script = ExtResource("13_lwhwy")
+
+[node name="ViewCamera" type="Camera2D" parent="ViewportPanel/ViewportContainer/Viewport"]
+anchor_mode = 0
+editor_draw_screen = false
+script = ExtResource("14_yjb74")
+
+[connection signal="pressed" from="PanelContainer/HBoxContainer/LeftMenu/MoreOptions" to="." method="_on_more_options_pressed"]
+[connection signal="pressed" from="PanelContainer/HBoxContainer/LeftMenu/Settings" to="." method="_on_settings_pressed"]
+[connection signal="pressed" from="PanelContainer/HBoxContainer/LeftMenu/Visuals" to="." method="_on_visuals_button_pressed"]
+[connection signal="toggled" from="PanelContainer/HBoxContainer/LeftMenu/Snapping/SnapButton" to="." method="_on_snap_button_toggled"]
+[connection signal="value_changed" from="PanelContainer/HBoxContainer/LeftMenu/Snapping/NumberEdit" to="." method="_on_number_edit_value_changed"]
+[connection signal="zoom_changed" from="PanelContainer/HBoxContainer/ZoomMenu" to="ViewportPanel/ViewportContainer/Viewport" method="_on_zoom_changed"]
+[connection signal="zoom_reset_pressed" from="PanelContainer/HBoxContainer/ZoomMenu" to="ViewportPanel/ViewportContainer/Viewport" method="center_frame"]
+[connection signal="size_changed" from="ViewportPanel/ViewportContainer/Viewport" to="ViewportPanel/ViewportContainer/Viewport" method="_on_size_changed"]
diff --git a/src/ui_parts/display_texture.gd b/src/ui_parts/display_texture.gd
new file mode 100644
index 000000000..54c0e2f54
--- /dev/null
+++ b/src/ui_parts/display_texture.gd
@@ -0,0 +1,69 @@
+extends TextureRect
+
+var view_rect := Rect2():
+ set(new_value):
+ if view_rect != new_value:
+ view_rect = new_value
+ queue_update()
+
+var rasterized := false:
+ set(new_value):
+ if new_value != rasterized:
+ rasterized = new_value
+ if Indications.zoom != 1.0:
+ queue_update()
+
+var update_pending := false
+
+
+func _ready() -> void:
+ SVG.root_tag.tag_layout_changed.connect(queue_update)
+ SVG.root_tag.changed_unknown.connect(queue_update)
+ SVG.root_tag.attribute_changed.connect(queue_update.unbind(1))
+ SVG.root_tag.child_attribute_changed.connect(queue_update.unbind(1))
+ Indications.zoom_changed.connect(queue_update)
+ queue_update()
+
+
+func queue_update() -> void:
+ update_pending = true
+
+func _process(_delta: float) -> void:
+ if update_pending:
+ svg_update()
+ update_pending = false
+
+
+func svg_update() -> void:
+ var image_zoom := 1.0 if rasterized and Indications.zoom > 1.0 else Indications.zoom
+ var pixel_size := 1 / image_zoom
+
+ # This will only be used for the display SVG, so we just need the text describing it.
+ # We also only need to change the dimensions and viewBox.
+ # So we'll change the SVG tag and directly parse the rest as text for performance.
+ var svg_tag := TagSVG.new()
+
+ # Translate to canvas coords.
+ var display_rect := view_rect.grow(pixel_size * 2)
+ display_rect.position = display_rect.position.snapped(Vector2(pixel_size, pixel_size))
+ display_rect.position.x = maxf(display_rect.position.x, 0.0)
+ display_rect.position.y = maxf(display_rect.position.y, 0.0)
+ display_rect.size = display_rect.size.snapped(Vector2(pixel_size, pixel_size))
+ display_rect.end.x = minf(display_rect.end.x, SVG.root_tag.width)
+ display_rect.end.y = minf(display_rect.end.y, SVG.root_tag.height)
+
+ svg_tag.attributes.viewBox.set_rect(Rect2(
+ SVG.root_tag.world_to_canvas(display_rect.position),
+ display_rect.size / SVG.root_tag.canvas_transform.get_scale()))
+ svg_tag.attributes.width.set_num(display_rect.size.x)
+ svg_tag.attributes.height.set_num(display_rect.size.y)
+
+ var svg_text := SVGParser.svg_to_text(svg_tag)
+ for tag in SVG.root_tag.child_tags:
+ svg_text += SVGParser._tag_to_text(tag)
+
+ position = display_rect.position
+ size = display_rect.size
+ var img := Image.new()
+ img.load_svg_from_string(svg_text, image_zoom)
+ texture = ImageTexture.create_from_image(img)
diff --git a/src/ui_parts/docs.gd b/src/ui_parts/docs.gd
new file mode 100644
index 000000000..590bcc841
--- /dev/null
+++ b/src/ui_parts/docs.gd
@@ -0,0 +1,13 @@
+extends PanelContainer
+
+@onready var desc: RichTextLabel = %"#kbd_shortcuts_tab"
+
+func _ready() -> void:
+ for shortcut in [&"#shortcut_inspector_delete", &"#shortcut_inspector_ctrl_down",
+ &"#shortcut_inspector_ctrl_up", &"#shortcut_inspector_ctrl_d"]:
+ desc.add_text(tr(shortcut))
+ desc.newline()
+
+
+func _on_close_pressed() -> void:
+ queue_free()
diff --git a/src/ui_parts/docs.tscn b/src/ui_parts/docs.tscn
new file mode 100644
index 000000000..6b9016cff
--- /dev/null
+++ b/src/ui_parts/docs.tscn
@@ -0,0 +1,59 @@
+[gd_scene load_steps=4 format=3 uid="uid://bgaywfst262tr"]
+
+[ext_resource type="Script" path="res://src/ui_parts/docs.gd" id="1_v0u3p"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterTabContainer.gd" id="2_h5w1r"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cr53y"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(0.005, 0.005, 0.05, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.203922, 0.254902, 0.4, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[node name="Docs" type="PanelContainer"]
+custom_minimum_size = Vector2(320, 160)
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -117.0
+offset_top = -46.0
+offset_right = 117.0
+offset_bottom = 46.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_cr53y")
+script = ExtResource("1_v0u3p")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="TabContainer" type="TabContainer" parent="VBoxContainer"]
+layout_mode = 2
+script = ExtResource("2_h5w1r")
+
+[node name="#kbd_shortcuts_tab" type="RichTextLabel" parent="VBoxContainer/TabContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 96)
+layout_mode = 2
+
+[node name="Close" type="Button" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 8
+focus_mode = 0
+mouse_default_cursor_shape = 2
+text = "#close"
+
+[connection signal="pressed" from="VBoxContainer/Close" to="." method="_on_close_pressed"]
diff --git a/src/ui_parts/export_dialog.gd b/src/ui_parts/export_dialog.gd
new file mode 100644
index 000000000..aba83bda6
--- /dev/null
+++ b/src/ui_parts/export_dialog.gd
@@ -0,0 +1,99 @@
+extends PanelContainer
+
+const NumberEditType = preload("res://src/ui_elements/number_edit.gd")
+const SVGFileDialog = preload("res://src/ui_parts/svg_file_dialog.tscn")
+
+var upscale_amount := -1.0
+var extension := ""
+var dimensions := Vector2.ZERO
+
+@onready var dimensions_label: Label = %DimensionsLabel
+@onready var texture_preview: TextureRect = %TexturePreview
+@onready var dropdown: HBoxContainer = %Dropdown
+@onready var final_dimensions_label: Label = %FinalDimensions
+@onready var scale_edit: NumberEditType = %Scale
+@onready var scale_container: VBoxContainer = %ScaleContainer
+
+func _ready() -> void:
+ scale_edit.value_changed.connect(_on_scale_value_changed)
+ dropdown.value_changed.connect(_on_dropdown_value_changed)
+ extension = dropdown.value
+ update_extension_configuration()
+ dimensions = SVG.root_tag.get_size()
+ scale_edit.min_value = 1/minf(dimensions.x, dimensions.y)
+ scale_edit.max_value = 16384/maxf(dimensions.x, dimensions.y)
+ scale_edit.set_value(minf(scale_edit.get_value(),
+ 2048/maxf(dimensions.x, dimensions.y)))
+ update_dimensions_label()
+ update_final_scale()
+ var scaling_factor := texture_preview.size.x * 2.0 / maxf(dimensions.x, dimensions.y)
+ var img := Image.new()
+ img.load_svg_from_string(SVG.text, scaling_factor)
+ if not img.is_empty():
+ img.fix_alpha_edges()
+ texture_preview.texture = ImageTexture.create_from_image(img)
+
+
+func update_dimensions_label() -> void:
+ dimensions_label.text = tr(&"#size") + ": " + NumberParser.num_to_text(dimensions.x) +\
+ "Ć" + NumberParser.num_to_text(dimensions.y)
+
+func _on_dropdown_value_changed(new_value: String) -> void:
+ extension = new_value
+ update_extension_configuration()
+
+
+func native_file_export(has_selected: bool, files: PackedStringArray,
+_filter_idx: int) -> void:
+ if has_selected:
+ export(files[0])
+
+func _on_ok_button_pressed() -> void:
+ if OS.has_feature("web"):
+ match extension:
+ "png":
+ HandlerGUI.web_save_png(_create_img())
+ _:
+ HandlerGUI.web_save_svg()
+ else:
+ SVG.open_save_dialog(extension, native_file_export, export)
+
+func export(path: String) -> void:
+ if path.get_extension().is_empty():
+ path += "." + extension
+
+ GlobalSettings.modify_save_data(&"last_used_dir", path.get_base_dir())
+
+ match extension:
+ "png":
+ _create_img().save_png(path)
+ _:
+ # SVG / fallback.
+ GlobalSettings.modify_save_data(&"current_file_path", path)
+ SVG.save_svg_to_file(path)
+ queue_free()
+
+func _on_cancel_button_pressed() -> void:
+ queue_free()
+
+
+func _on_scale_value_changed(_new_value: float) -> void:
+ update_final_scale()
+
+func update_final_scale() -> void:
+ upscale_amount = scale_edit.get_value()
+ var exported_size: Vector2i = dimensions * upscale_amount
+ final_dimensions_label.text = tr(&"#final_size") +\
+ ": %dĆ%d" % [exported_size.x, exported_size.y]
+
+func update_extension_configuration() -> void:
+ scale_container.visible = (extension == "png")
+
+func _create_img() -> Image:
+ var export_svg := SVG.root_tag.create_duplicate()
+ export_svg.attributes.width.set_num(export_svg.width * upscale_amount)
+ export_svg.attributes.height.set_num(export_svg.height * upscale_amount)
+ var img := Image.new()
+ img.load_svg_from_string(SVGParser.svg_to_text(export_svg))
+ img.fix_alpha_edges() # See godot issue 82579.
+ return img
diff --git a/src/ui_parts/export_dialog.tscn b/src/ui_parts/export_dialog.tscn
new file mode 100644
index 000000000..13becef81
--- /dev/null
+++ b/src/ui_parts/export_dialog.tscn
@@ -0,0 +1,144 @@
+[gd_scene load_steps=8 format=3 uid="uid://c13dadqbljqlu"]
+
+[ext_resource type="Script" path="res://src/ui_parts/export_dialog.gd" id="1_objnb"]
+[ext_resource type="Shader" path="res://src/shaders/zoom_shader.gdshader" id="2_6bte3"]
+[ext_resource type="Texture2D" uid="uid://c68og6bsqt0lb" path="res://visual/icons/backgrounds/Checkerboard.svg" id="3_q8p2p"]
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="4_4t2iq"]
+[ext_resource type="PackedScene" uid="uid://dbu1lvajypafb" path="res://src/ui_elements/dropdown.tscn" id="5_y6ex0"]
+[ext_resource type="PackedScene" uid="uid://dad7fkhmsooc6" path="res://src/ui_elements/number_edit.tscn" id="6_w1sag"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_y7eee"]
+shader = ExtResource("2_6bte3")
+shader_parameter/uv_scale = 2.0
+
+[node name="ExportDialog" type="PanelContainer"]
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -2.0
+offset_top = -2.0
+offset_right = 2.0
+offset_bottom = 2.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_objnb")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 8
+theme_override_constants/margin_bottom = 8
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "#export_configuration"
+horizontal_alignment = 1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="Checkerboard" type="TextureRect" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+material = SubResource("ShaderMaterial_y7eee")
+custom_minimum_size = Vector2(128, 128)
+layout_mode = 2
+texture = ExtResource("3_q8p2p")
+stretch_mode = 1
+
+[node name="TexturePreview" type="TextureRect" parent="MarginContainer/VBoxContainer/HBoxContainer/Checkerboard"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+expand_mode = 2
+stretch_mode = 5
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+custom_minimum_size = Vector2(160, 0)
+layout_mode = 2
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_top = 6
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 6
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="DimensionsLabel" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_fonts/font = ExtResource("4_4t2iq")
+horizontal_alignment = 1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+text = "#format"
+
+[node name="Dropdown" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer/HBoxContainer" instance=ExtResource("5_y6ex0")]
+unique_name_in_owner = true
+layout_mode = 2
+values = Array[String](["svg", "png"])
+
+[node name="ScaleContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer/ScaleContainer"]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer/ScaleContainer/HBoxContainer"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+text = "#scale"
+
+[node name="Scale" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer/ScaleContainer/HBoxContainer" instance=ExtResource("6_w1sag")]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(46, 22)
+layout_mode = 2
+initial_value = 8.0
+allow_lower = false
+allow_higher = false
+
+[node name="FinalDimensions" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/MarginContainer/VBoxContainer/ScaleContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_colors/font_color = Color(0.666667, 0.666667, 0.666667, 1)
+theme_override_fonts/font = ExtResource("4_4t2iq")
+theme_override_font_sizes/font_size = 10
+
+[node name="ButtonContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+alignment = 1
+
+[node name="CancelButton" type="Button" parent="MarginContainer/VBoxContainer/ButtonContainer"]
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+text = "#cancel"
+
+[node name="OKButton" type="Button" parent="MarginContainer/VBoxContainer/ButtonContainer"]
+layout_mode = 2
+focus_mode = 0
+mouse_default_cursor_shape = 2
+text = "#export"
+
+[connection signal="pressed" from="MarginContainer/VBoxContainer/ButtonContainer/CancelButton" to="." method="_on_cancel_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/ButtonContainer/OKButton" to="." method="_on_ok_button_pressed"]
diff --git a/src/ui_parts/handles_manager.gd b/src/ui_parts/handles_manager.gd
new file mode 100644
index 000000000..45eb9e183
--- /dev/null
+++ b/src/ui_parts/handles_manager.gd
@@ -0,0 +1,763 @@
+## Contours drawing and [Handle]s are managed here.
+extends Control
+
+const PathCommandPopup = preload("res://src/ui_elements/path_popup.tscn")
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const ContextPopupType = preload("res://src/ui_elements/context_popup.gd")
+
+const handle_texture_dir = "res://visual/icons/handles/%s.svg"
+
+const normal_handle_textures = {
+ Handle.Display.BIG: preload(handle_texture_dir % "HandleBig"),
+ Handle.Display.SMALL: preload(handle_texture_dir % "HandleSmall"),
+}
+
+const hovered_handle_textures = {
+ Handle.Display.BIG: preload(handle_texture_dir % "HandleBigHovered"),
+ Handle.Display.SMALL: preload(handle_texture_dir % "HandleSmallHovered"),
+}
+
+const selected_handle_textures = {
+ Handle.Display.BIG: preload(handle_texture_dir % "HandleBigSelected"),
+ Handle.Display.SMALL: preload(handle_texture_dir % "HandleSmallSelected"),
+}
+
+const hovered_selected_handle_textures = {
+ Handle.Display.BIG: preload(handle_texture_dir % "HandleBigHoveredSelected"),
+ Handle.Display.SMALL: preload(handle_texture_dir % "HandleSmallHoveredSelected"),
+}
+
+const default_color_string = "#000"
+const hover_color_string = "#aaa"
+const selection_color_string = "#46f"
+const hover_selection_color_string = "#f44"
+const default_color = Color(default_color_string)
+const hover_color = Color(hover_color_string)
+const selection_color = Color(selection_color_string)
+const hover_selection_color = Color(hover_selection_color_string)
+
+var update_pending := false
+
+var handles: Array[Handle]
+
+var surface := RenderingServer.canvas_item_create()
+
+func _ready() -> void:
+ RenderingServer.canvas_item_set_parent(surface, get_canvas_item())
+ SVG.root_tag.attribute_changed.connect(queue_update.unbind(1))
+ SVG.root_tag.child_attribute_changed.connect(queue_redraw.unbind(1))
+ SVG.root_tag.child_attribute_changed.connect(sync_handles.unbind(1))
+ SVG.root_tag.tag_layout_changed.connect(queue_update)
+ SVG.root_tag.changed_unknown.connect(queue_update)
+ Indications.selection_changed.connect(queue_redraw)
+ Indications.hover_changed.connect(queue_redraw)
+ Indications.zoom_changed.connect(queue_redraw)
+ Indications.added_handle.connect(move_selected_to_mouse)
+ queue_update()
+
+
+func queue_update() -> void:
+ update_pending = true
+
+func _process(_delta: float) -> void:
+ if update_pending:
+ update_handles()
+ update_pending = false
+
+
+func update_handles() -> void:
+ handles.clear()
+ for tid in SVG.root_tag.get_all_tids():
+ var tag := SVG.root_tag.get_tag(tid)
+ match tag.name:
+ "circle":
+ handles.append(generate_xy_handle(tid, tag, "cx", "cy", "transform"))
+ handles.append(generate_delta_handle(tid, tag, "cx", "cy", "transform",
+ "r", true))
+ "ellipse":
+ handles.append(generate_xy_handle(tid, tag, "cx", "cy", "transform"))
+ handles.append(generate_delta_handle(tid, tag, "cx", "cy", "transform",
+ "rx", true))
+ handles.append(generate_delta_handle(tid, tag, "cx", "cy", "transform",
+ "ry", false))
+ "rect":
+ handles.append(generate_xy_handle(tid, tag, "x", "y", "transform"))
+ handles.append(generate_xy_handle(tid, tag, "x", "y", "transform"))
+ handles.append(generate_delta_handle(tid, tag, "x", "y", "transform",
+ "width", true))
+ handles.append(generate_delta_handle(tid, tag, "x", "y", "transform",
+ "height", false))
+ "line":
+ handles.append(generate_xy_handle(tid, tag, "x1", "y1", "transform"))
+ handles.append(generate_xy_handle(tid, tag, "x2", "y2", "transform"))
+ "path":
+ handles += generate_path_handles(tid, tag.attributes.d,
+ tag.attributes.transform)
+ # Pretend the mouse was moved to update the hovering.
+ var mouse_motion_event := InputEventMouseMotion.new()
+ mouse_motion_event.position = get_viewport().get_mouse_position()
+ respond_to_input_event(mouse_motion_event)
+ queue_redraw()
+
+
+func sync_handles() -> void:
+ # For XYHandles, sync them. For PathHandles, they can be added and removed as an
+ # attribute changes, so remove them and re-add them except for the dragged one.
+ for handle_idx in range(handles.size() - 1, -1, -1):
+ var handle := handles[handle_idx]
+ if handle is PathHandle:
+ if dragged_handle != handle:
+ handles.remove_at(handle_idx)
+ else:
+ handle.sync()
+
+ for tid in SVG.root_tag.get_all_tids():
+ var tag := SVG.root_tag.get_tag(tid)
+ if tag.name == "path":
+ handles += generate_path_handles(tid, tag.attributes.d, tag.attributes.transform)
+ queue_redraw()
+
+func generate_path_handles(tid: PackedInt32Array, data_attrib: AttributePath,
+t_attrib: AttributeTransform) -> Array[Handle]:
+ var path_handles: Array[Handle] = []
+ for idx in data_attrib.get_command_count():
+ var path_command := data_attrib.get_command(idx)
+ if not path_command.command_char in "Zz":
+ path_handles.append(PathHandle.new(tid, data_attrib, t_attrib, idx))
+ if path_command.command_char in "CcQq":
+ var tangent := PathHandle.new(tid, data_attrib, t_attrib, idx, &"x1", &"y1")
+ tangent.display_mode = Handle.Display.SMALL
+ path_handles.append(tangent)
+ if path_command.command_char in "CcSs":
+ var tangent := PathHandle.new(tid, data_attrib, t_attrib, idx, &"x2", &"y2")
+ tangent.display_mode = Handle.Display.SMALL
+ path_handles.append(tangent)
+ return path_handles
+
+# Helpers for generating the handles when the tag is at hand.
+func generate_xy_handle(tid: PackedInt32Array, tag: Tag, x_attrib_name: String,\
+y_attrib_name: String, t_attrib_name: String) -> XYHandle:
+ return XYHandle.new(tid, tag.attributes[x_attrib_name],
+ tag.attributes[y_attrib_name], tag.attributes[t_attrib_name])
+
+func generate_delta_handle(tid: PackedInt32Array, tag: Tag, x_attrib_name: String,\
+y_attrib_name: String, t_attrib_name: String, delta_attrib_name: String,\
+horizontal: bool) -> DeltaHandle:
+ return DeltaHandle.new(tid, tag.attributes[x_attrib_name],
+ tag.attributes[y_attrib_name], tag.attributes[t_attrib_name],
+ tag.attributes[delta_attrib_name], horizontal)
+
+
+func _draw() -> void:
+ # Store contours of shapes.
+ var normal_polylines: Array[PackedVector2Array] = []
+ var selected_polylines: Array[PackedVector2Array] = []
+ var hovered_polylines: Array[PackedVector2Array] = []
+ var hovered_selected_polylines: Array[PackedVector2Array] = []
+ # Store abstract contours, e.g. tangents.
+ var normal_multiline := PackedVector2Array()
+ var selected_multiline := PackedVector2Array()
+ var hovered_multiline := PackedVector2Array()
+ var hovered_selected_multiline := PackedVector2Array()
+
+ for tid in SVG.root_tag.get_all_tids():
+ var tag := SVG.root_tag.get_tag(tid)
+ var attribs := tag.attributes
+
+ # Determine if the tag is hovered/selected or has a hovered/selected parent.
+ var tag_hovered := tid_is_hovered(tid, -1)
+ var tag_selected := tid_is_selected(tid, -1)
+
+ match tag.name:
+ "circle":
+ var c := Vector2(attribs.cx.get_num(), attribs.cy.get_num())
+ var r: float = attribs.r.get_num()
+
+ var points := PackedVector2Array()
+ points.resize(181)
+ for i in 180:
+ var d := i * TAU/180
+ points[i] = c + Vector2(cos(d), sin(d)) * r
+ points[180] = points[0]
+ var extras := PackedVector2Array([c, c + Vector2(r, 0)])
+ points = attribs.transform.get_final_transform() * points
+ extras = attribs.transform.get_final_transform() * extras
+
+ if tag_hovered and tag_selected:
+ hovered_selected_polylines.append(points)
+ hovered_selected_multiline += extras
+ elif tag_hovered:
+ hovered_polylines.append(points)
+ hovered_multiline += extras
+ elif tag_selected:
+ selected_polylines.append(points)
+ selected_multiline += extras
+ else:
+ normal_polylines.append(points)
+ normal_multiline += extras
+
+ "ellipse":
+ var c := Vector2(attribs.cx.get_num(), attribs.cy.get_num())
+ var rx: float = attribs.rx.get_num()
+ var ry: float = attribs.ry.get_num()
+ # Squished circle.
+ var points := PackedVector2Array()
+ points.resize(181)
+ for i in 180:
+ var d := i * TAU/180
+ points[i] = c + Vector2(cos(d) * rx, sin(d) * ry)
+ points[180] = points[0]
+ var extras := PackedVector2Array([
+ c, c + Vector2(rx, 0), c, c + Vector2(0, ry)])
+ points = attribs.transform.get_final_transform() * points
+ extras = attribs.transform.get_final_transform() * extras
+
+ if tag_hovered and tag_selected:
+ hovered_selected_polylines.append(points)
+ hovered_selected_multiline += extras
+ elif tag_hovered:
+ hovered_polylines.append(points)
+ hovered_multiline += extras
+ elif tag_selected:
+ selected_polylines.append(points)
+ selected_multiline += extras
+ else:
+ normal_polylines.append(points)
+ normal_multiline += extras
+
+ "rect":
+ var x: float = attribs.x.get_num()
+ var y: float = attribs.y.get_num()
+ var rect_width: float = attribs.width.get_num()
+ var rect_height: float = attribs.height.get_num()
+ var rx: float = attribs.rx.get_num()
+ var ry: float = attribs.ry.get_num()
+ var points := PackedVector2Array()
+ if rx == 0 and ry == 0:
+ # Basic rectangle.
+ points = [Vector2(x, y), Vector2(x + rect_width, y),
+ Vector2(x + rect_width, y + rect_height),
+ Vector2(x, y + rect_height), Vector2(x, y)]
+ else:
+ if rx == 0:
+ rx = ry
+ elif ry == 0:
+ ry = rx
+ rx = minf(rx, rect_width / 2)
+ ry = minf(ry, rect_height / 2)
+ # Rounded rectangle.
+ points.resize(186)
+ points[0] = Vector2(x + rx, y)
+ points[1] = Vector2(x + rect_width - rx, y)
+ for i in range(135, 180):
+ var d := i * TAU/180
+ points[i - 133] = Vector2(x + rect_width - rx, y + ry) +\
+ Vector2(cos(d) * rx, sin(d) * ry)
+ points[47] = Vector2(x + rect_width, y + rect_height - ry)
+ for i in range(0, 45):
+ var d := i * TAU/180
+ points[i + 48] = Vector2(x + rect_width - rx, y + rect_height - ry) +\
+ Vector2(cos(d) * rx, sin(d) * ry)
+ points[93] = Vector2(x + rx, y + rect_height)
+ for i in range(45, 90):
+ var d := i * TAU/180
+ points[i + 49] = Vector2(x + rx, y + rect_height - ry) +\
+ Vector2(cos(d) * rx, sin(d) * ry)
+ points[139] = Vector2(x, y + ry)
+ for i in range(90, 135):
+ var d := i * TAU/180
+ points[i + 50] = Vector2(x + rx, y + ry) +\
+ Vector2(cos(d) * rx, sin(d) * ry)
+ points[185] = points[0]
+ var extras := PackedVector2Array([Vector2(x, y), Vector2(x + rect_width, y),
+ Vector2(x, y), Vector2(x, y + rect_height)])
+ points = attribs.transform.get_final_transform() * points
+ extras = attribs.transform.get_final_transform() * extras
+
+ if tag_hovered and tag_selected:
+ hovered_selected_polylines.append(points)
+ hovered_selected_multiline += extras
+ elif tag_hovered:
+ hovered_polylines.append(points)
+ hovered_multiline += extras
+ elif tag_selected:
+ selected_polylines.append(points)
+ selected_multiline += extras
+ else:
+ normal_polylines.append(points)
+ normal_multiline += extras
+
+ "line":
+ var x1: float = attribs.x1.get_num()
+ var y1: float = attribs.y1.get_num()
+ var x2: float = attribs.x2.get_num()
+ var y2: float = attribs.y2.get_num()
+
+ var points := PackedVector2Array([Vector2(x1, y1), Vector2(x2, y2)])
+ points = attribs.transform.get_final_transform() * points
+
+ if tag_hovered and tag_selected:
+ hovered_selected_polylines.append(points)
+ elif tag_hovered:
+ hovered_polylines.append(points)
+ elif tag_selected:
+ selected_polylines.append(points)
+ else:
+ normal_polylines.append(points)
+
+ "path":
+ var pathdata: AttributePath = attribs.d
+ if pathdata.get_command_count() == 0 or\
+ not pathdata.get_command(0).command_char in "Mm":
+ continue # Nothing to draw.
+
+ var current_mode := Utils.InteractionType.NONE
+
+ for cmd_idx in pathdata.get_command_count():
+ # Drawing logic.
+ var points := PackedVector2Array()
+ var tangent_points := PackedVector2Array()
+ var cmd := pathdata.get_command(cmd_idx)
+ var relative := cmd.relative
+
+ current_mode = Utils.InteractionType.NONE
+ if tid_is_hovered(tid, cmd_idx):
+ @warning_ignore("int_as_enum_without_cast")
+ current_mode += Utils.InteractionType.HOVERED
+ if tid_is_selected(tid, cmd_idx):
+ @warning_ignore("int_as_enum_without_cast")
+ current_mode += Utils.InteractionType.SELECTED
+
+ match cmd.command_char.to_upper():
+ "L":
+ # Line contour.
+ var v := Vector2(cmd.x, cmd.y)
+ var end := cmd.start + v if relative else v
+ points = PackedVector2Array([cmd.start, end])
+ "H":
+ # Horizontal line contour.
+ var v := Vector2(cmd.x, 0)
+ var end := cmd.start + v if relative else Vector2(v.x, cmd.start.y)
+ points = PackedVector2Array([cmd.start, end])
+ "V":
+ # Vertical line contour.
+ var v := Vector2(0, cmd.y)
+ var end := cmd.start + v if relative else Vector2(cmd.start.x, v.y)
+ points = PackedVector2Array([cmd.start, end])
+ "C":
+ # Cubic Bezier curve contour.
+ var v := Vector2(cmd.x, cmd.y)
+ var v1 := Vector2(cmd.x1, cmd.y1)
+ var v2 := Vector2(cmd.x2, cmd.y2)
+ var cp1 := cmd.start
+ var cp4 := cp1 + v if relative else v
+ var cp2 := v1 if relative else v1 - cp1
+ var cp3 := v2 - v
+
+ points = Utils.get_cubic_bezier_points(cp1, cp2, cp3, cp4)
+ tangent_points.append_array(PackedVector2Array([cp1,
+ cp1 + cp2, cp1 + v2 if relative else v2, cp4]))
+ "S":
+ # Shorthand cubic Bezier curve contour.
+ if cmd_idx == 0:
+ break
+
+ var v := Vector2(cmd.x, cmd.y)
+ var v1 := pathdata.get_implied_S_control(cmd_idx)
+ var v2 := Vector2(cmd.x2, cmd.y2)
+
+ var cp1 := cmd.start
+ var cp4 := cp1 + v if relative else v
+ var cp2 := v1 if relative else v1 - cp1
+ var cp3 := v2 - v
+
+ points = Utils.get_cubic_bezier_points(cp1, cp2, cp3, cp4)
+ tangent_points.append_array(PackedVector2Array([cp1,
+ cp1 + cp2, cp1 + v2 if relative else v2, cp4]))
+ "Q":
+ # Quadratic Bezier curve contour.
+ var v := Vector2(cmd.x, cmd.y)
+ var v1 := Vector2(cmd.x1, cmd.y1)
+ var cp1 := cmd.start
+ var cp2 := cp1 + v1 if relative else v1
+ var cp3 := cp1 + v if relative else v
+
+ points = Utils.get_quadratic_bezier_points(cp1, cp2, cp3)
+ tangent_points.append_array(PackedVector2Array([cp1, cp2, cp2, cp3]))
+ "T":
+ # Shorthand quadratic Bezier curve contour.
+ var v := Vector2(cmd.x, cmd.y)
+ var v1 := pathdata.get_implied_T_control(cmd_idx)
+
+ var cp1 := cmd.start
+ var cp2 := v1 + cp1 if relative else v1
+ var cp3 := cp1 + v if relative else v
+
+ if is_nan(cp2.x) and is_nan(cp2.y):
+ points = PackedVector2Array([cp1, cp3])
+ else:
+ points = Utils.get_quadratic_bezier_points(cp1, cp2, cp3)
+ tangent_points.append_array(
+ PackedVector2Array([cp1, cp2, cp2, cp3]))
+ "A":
+ # Elliptical arc contour.
+ var start := cmd.start
+ var v := Vector2(cmd.x, cmd.y)
+ var end := start + v if relative else v
+ # Correct for out-of-range radii.
+ if start == end:
+ continue
+ elif cmd.rx == 0 or cmd.ry == 0:
+ points = PackedVector2Array([start, end])
+
+ var r := Vector2(cmd.rx, cmd.ry).abs()
+ # Obtain center parametrization.
+ var rot := deg_to_rad(cmd.rot)
+ var cosine := cos(rot)
+ var sine := sin(rot)
+ var half := (start - end) / 2
+ var x1 := half.x * cosine + half.y * sine
+ var y1 := -half.x * sine + half.y * cosine
+ var r2 := Vector2(r.x * r.x, r.y * r.y)
+ var x12 := x1 * x1
+ var y12 := y1 * y1
+ var cr := x12 / r2.x + y12 / r2.y
+ if cr > 1:
+ cr = sqrt(cr)
+ r *= cr
+ r2 = Vector2(r.x * r.x, r.y * r.y)
+
+ var dq := r2.x * y12 + r2.y * x12
+ var pq := (r2.x * r2.y - dq) / dq
+ var sc := sqrt(maxf(0, pq))
+ if cmd.large_arc_flag == cmd.sweep_flag:
+ sc = -sc
+
+ var ct := Vector2(r.x * sc * y1 / r.y, -r.y * sc * x1 / r.x)
+ var c := Vector2(ct.x * cosine - ct.y * sine,
+ ct.x * sine + ct.y * cosine) + start.lerp(end, 0.5)
+ var tv := Vector2(x1 - ct.x, y1 - ct.y) / r
+ var theta1 := tv.angle()
+ var delta_theta := fposmod(tv.angle_to(
+ Vector2(-x1 - ct.x, -y1 - ct.y) / r), TAU)
+ if cmd.sweep_flag == 0:
+ theta1 += delta_theta
+ delta_theta = TAU - delta_theta
+
+ # Now we have a center parametrization (r, c, theta1, delta_theta).
+ # We will approximate the elliptical arc with Bezier curves.
+ # Use the method described in https://www.blog.akhil.cc/ellipse
+ # (but with modifications because it wasn't working fully).
+ var segments := delta_theta * 4/PI
+ var n := floori(segments)
+ var p1 := Utils.E(c, r, cosine, sine, theta1)
+ var e1 := Utils.Et(r, cosine, sine, theta1)
+ var alpha := 0.26511478
+ var t := theta1 + PI/4
+ var cp: Array[PackedVector2Array] = []
+ for _i in n:
+ var p2 := Utils.E(c, r, cosine, sine, t)
+ var e2 := Utils.Et(r, cosine, sine, t)
+ cp.append(PackedVector2Array([p1, alpha * e1, -alpha * e2, p2]))
+ p1 = p2
+ e1 = e2
+ t += PI/4
+
+ if n != ceili(segments):
+ t = theta1 + delta_theta
+ var p2 := Utils.E(c, r, cosine, sine, t)
+ var e2 := Utils.Et(r, cosine, sine, t)
+ alpha *= fposmod(delta_theta, PI/4) / (PI/4)
+ cp.append(PackedVector2Array([p1, alpha * e1, -alpha * e2, p2]))
+
+ for p in cp:
+ points += Utils.get_cubic_bezier_points(p[0], p[1], p[2], p[3])
+ "Z":
+ # Path closure contour.
+ var prev_M_idx := cmd_idx - 1
+ var prev_M_cmd := pathdata.get_command(prev_M_idx)
+ while prev_M_idx >= 0:
+ if prev_M_cmd.command_char in "Mm":
+ break
+ prev_M_idx -= 1
+ prev_M_cmd = pathdata.get_command(prev_M_idx)
+ if prev_M_idx == -1:
+ break
+
+ var end := Vector2(prev_M_cmd.x, prev_M_cmd.y)
+ if prev_M_cmd.relative:
+ end += prev_M_cmd.start
+
+ points = PackedVector2Array([cmd.start, end])
+ _: continue
+ points = attribs.transform.get_final_transform() * points
+ tangent_points = attribs.transform.get_final_transform() * tangent_points
+ match current_mode:
+ Utils.InteractionType.NONE:
+ normal_polylines.append(points.duplicate())
+ normal_multiline += tangent_points.duplicate()
+ Utils.InteractionType.HOVERED:
+ hovered_polylines.append(points.duplicate())
+ hovered_multiline += tangent_points.duplicate()
+ Utils.InteractionType.SELECTED:
+ selected_polylines.append(points.duplicate())
+ selected_multiline += tangent_points.duplicate()
+ Utils.InteractionType.HOVERED_SELECTED:
+ hovered_selected_polylines.append(points.duplicate())
+ hovered_selected_multiline += tangent_points.duplicate()
+
+ var draw_zoom := Indications.zoom * SVG.root_tag.canvas_transform.get_scale().x
+ var contour_width := 1.0 / draw_zoom
+ var tangent_width := 0.6 / draw_zoom
+ var tangent_alpha := 0.8
+ draw_set_transform_matrix(SVG.root_tag.canvas_transform)
+ RenderingServer.canvas_item_set_transform(surface, Transform2D(0.0,
+ Vector2(1 / Indications.zoom, 1 / Indications.zoom), 0.0, Vector2.ZERO))
+
+ for polyline in normal_polylines:
+ draw_polyline(polyline, default_color, contour_width, true)
+ for polyline in selected_polylines:
+ draw_polyline(polyline, selection_color, contour_width, true)
+ for polyline in hovered_polylines:
+ draw_polyline(polyline, hover_color, contour_width, true)
+ for polyline in hovered_selected_polylines:
+ draw_polyline(polyline, hover_selection_color, contour_width, true)
+
+ # TODO Change this when it's implemented in Godot.
+ draw_multiline_antaliased(normal_multiline,
+ Color(default_color, tangent_alpha), tangent_width)
+ draw_multiline_antaliased(selected_multiline,
+ Color(selection_color, tangent_alpha), tangent_width)
+ draw_multiline_antaliased(hovered_multiline,
+ Color(hover_color, tangent_alpha), tangent_width)
+ draw_multiline_antaliased(hovered_selected_multiline,
+ Color(hover_selection_color, tangent_alpha), tangent_width)
+
+ # First gather all handles in 4 categories, then draw them in the right order.
+ var normal_handles: Array[Handle] = []
+ var selected_handles: Array[Handle] = []
+ var hovered_handles: Array[Handle] = []
+ var hovered_selected_handles: Array[Handle] = []
+ for handle in handles:
+ var cmd_idx: int = handle.command_index if handle is PathHandle else -1
+ var is_hovered := tid_is_hovered(handle.tid, cmd_idx)
+ var is_selected := tid_is_selected(handle.tid, cmd_idx)
+
+ if is_hovered and is_selected:
+ hovered_selected_handles.append(handle)
+ elif is_hovered:
+ hovered_handles.append(handle)
+ elif is_selected:
+ selected_handles.append(handle)
+ else:
+ normal_handles.append(handle)
+
+ RenderingServer.canvas_item_clear(surface)
+ for handle in normal_handles:
+ var texture: Texture2D = normal_handle_textures[handle.display_mode]
+ texture.draw(surface, SVG.root_tag.canvas_to_world(handle.transform * handle.pos) *\
+ Indications.zoom - texture.get_size() / 2)
+ for handle in selected_handles:
+ var texture: Texture2D = selected_handle_textures[handle.display_mode]
+ texture.draw(surface, SVG.root_tag.canvas_to_world(handle.transform * handle.pos) *\
+ Indications.zoom - texture.get_size() / 2)
+ for handle in hovered_handles:
+ var texture: Texture2D = hovered_handle_textures[handle.display_mode]
+ texture.draw(surface, SVG.root_tag.canvas_to_world(handle.transform * handle.pos) *\
+ Indications.zoom - texture.get_size() / 2)
+ for handle in hovered_selected_handles:
+ var texture: Texture2D = hovered_selected_handle_textures[handle.display_mode]
+ texture.draw(surface, SVG.root_tag.canvas_to_world(handle.transform * handle.pos) *\
+ Indications.zoom - texture.get_size() / 2)
+
+# TODO remove this when it's implemented in Godot.
+func draw_multiline_antaliased(points: PackedVector2Array, color: Color,
+width: float) -> void:
+ for i in int(points.size() / 2.0):
+ var i2 := i * 2
+ draw_line(points[i2], points[i2 + 1], color, width, true)
+
+
+func tid_is_hovered(tid: PackedInt32Array, cmd_idx := -1) -> bool:
+ if cmd_idx == -1:
+ return Utils.is_tid_parent_or_self(Indications.hovered_tid, tid)
+ else:
+ return Utils.is_tid_parent_or_self(Indications.hovered_tid, tid) or\
+ (Indications.semi_hovered_tid == tid and Indications.inner_hovered == cmd_idx)
+
+func tid_is_selected(tid: PackedInt32Array, cmd_idx := -1) -> bool:
+ if cmd_idx == -1:
+ for selected_tid in Indications.selected_tids:
+ if Utils.is_tid_parent_or_self(selected_tid, tid):
+ return true
+ return false
+ else:
+ for selected_tid in Indications.selected_tids:
+ if Utils.is_tid_parent_or_self(selected_tid, tid):
+ return true
+ return Indications.semi_selected_tid == tid and\
+ cmd_idx in Indications.inner_selections
+
+
+var dragged_handle: Handle = null
+var hovered_handle: Handle = null
+var was_handle_moved := false
+var should_deselect_all := false
+
+func _unhandled_input(event: InputEvent) -> void:
+ respond_to_input_event(event)
+
+func respond_to_input_event(event: InputEvent) -> void:
+ if not visible:
+ return
+
+ # Set the nearest handle as hovered, if any handles are within range.
+ if (event is InputEventMouseMotion and dragged_handle == null and\
+ event.button_mask == 0) or (event is InputEventMouseButton and\
+ (event.button_index in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_WHEEL_DOWN,
+ MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_LEFT, MOUSE_BUTTON_WHEEL_RIGHT])):
+ var nearest_handle := find_nearest_handle(event.position / Indications.zoom +\
+ get_node(^"../..").view.position)
+ if nearest_handle != null:
+ hovered_handle = nearest_handle
+ if hovered_handle is PathHandle:
+ Indications.set_hovered(hovered_handle.tid, hovered_handle.command_index)
+ else:
+ Indications.set_hovered(hovered_handle.tid)
+ else:
+ hovered_handle = null
+ Indications.clear_hovered()
+ Indications.clear_inner_hovered()
+
+ var snap_enabled := GlobalSettings.save_data.snap > 0.0
+ var snap_size := absf(GlobalSettings.save_data.snap)
+ var snap_vector := Vector2(snap_size, snap_size)
+
+ if event is InputEventMouseMotion:
+ should_deselect_all = false
+ var event_pos: Vector2 = event.position / Indications.zoom +\
+ get_node(^"../..").view.position
+ if dragged_handle != null:
+ # Move the handle that's being dragged.
+ if snap_enabled:
+ event_pos = event_pos.snapped(snap_vector)
+ var new_pos := dragged_handle.transform.affine_inverse() *\
+ SVG.root_tag.world_to_canvas(event_pos)
+ dragged_handle.set_pos(new_pos)
+ was_handle_moved = true
+ accept_event()
+ elif event is InputEventMouseButton:
+ var event_pos: Vector2 = event.position / Indications.zoom +\
+ get_node(^"../..").view.position
+ if snap_enabled:
+ event_pos = event_pos.snapped(snap_vector)
+
+ if event.button_index == MOUSE_BUTTON_LEFT:
+ # React to LMB actions.
+ if hovered_handle != null and event.is_pressed():
+ dragged_handle = hovered_handle
+ dragged_handle.initial_pos = dragged_handle.pos
+ var inner_idx = -1
+ var dragged_tid := dragged_handle.tid
+ if dragged_handle is PathHandle:
+ inner_idx = dragged_handle.command_index
+
+ if event.double_click and inner_idx != -1:
+ # Unselect the tag, so then it's selected again in the subpath.
+ Indications.ctrl_select(dragged_tid, inner_idx)
+ var subpath_range: Vector2i =\
+ dragged_handle.path_attribute.get_subpath(inner_idx)
+ for idx in range(subpath_range.x, subpath_range.y + 1):
+ Indications.ctrl_select(dragged_tid, idx)
+ elif event.is_command_or_control_pressed():
+ Indications.ctrl_select(dragged_tid, inner_idx)
+ elif event.shift_pressed:
+ Indications.shift_select(dragged_tid, inner_idx)
+ else:
+ Indications.normal_select(dragged_tid, inner_idx)
+ elif dragged_handle != null and event.is_released():
+ if was_handle_moved:
+ var new_pos := dragged_handle.transform.affine_inverse() *\
+ SVG.root_tag.world_to_canvas(event_pos)
+ dragged_handle.set_pos(new_pos, true)
+ was_handle_moved = false
+ dragged_handle = null
+ elif hovered_handle == null and event.is_pressed():
+ should_deselect_all = true
+ elif hovered_handle == null and event.is_released() and should_deselect_all:
+ dragged_handle = null
+ Indications.clear_all_selections()
+ elif event.button_index == MOUSE_BUTTON_RIGHT:
+ var viewport := get_viewport()
+ var popup_pos := viewport.get_mouse_position()
+ if hovered_handle == null:
+ Indications.clear_all_selections()
+ Utils.popup_under_pos(create_tag_context(event_pos), popup_pos, viewport)
+ else:
+ var hovered_tid := hovered_handle.tid
+ var inner_idx = -1
+ if hovered_handle is PathHandle:
+ inner_idx = hovered_handle.command_index
+
+ if (Indications.semi_selected_tid != hovered_tid or\
+ not inner_idx in Indications.inner_selections) and\
+ not hovered_tid in Indications.selected_tids:
+ Indications.normal_select(hovered_tid, inner_idx)
+ Utils.popup_under_pos(Indications.get_selection_context(
+ Utils.popup_under_pos.bind(popup_pos, viewport)), popup_pos, viewport)
+
+func find_nearest_handle(event_pos: Vector2) -> Handle:
+ var nearest_handle: Handle = null
+ # Maximum grab distance is (9 / zoom).
+ var nearest_dist_squared := 81 / (Indications.zoom * Indications.zoom)
+ for handle in handles:
+ var dist_to_handle_squared := event_pos.distance_squared_to(
+ SVG.root_tag.canvas_to_world(handle.transform * handle.pos))
+ if dist_to_handle_squared < nearest_dist_squared:
+ nearest_dist_squared = dist_to_handle_squared
+ nearest_handle = handle
+ return nearest_handle
+
+func move_selected_to_mouse() -> void:
+ if not get_viewport_rect().has_point(get_viewport().get_mouse_position()):
+ return
+
+ for handle in handles:
+ if handle.tid == Indications.semi_selected_tid and handle is PathHandle and\
+ handle.command_index == Indications.inner_selections[0]:
+ Indications.set_hovered(handle.tid, handle.command_index)
+ dragged_handle = handle
+ # Move the handle that's being dragged.
+ var mouse_pos := get_global_mouse_position()
+ var snap_size := GlobalSettings.save_data.snap
+ if snap_size > 0.0:
+ mouse_pos = mouse_pos.snapped(Vector2(snap_size, snap_size))
+
+ var new_pos := dragged_handle.transform.affine_inverse() *\
+ SVG.root_tag.world_to_canvas(mouse_pos)
+ dragged_handle.set_pos(new_pos)
+ was_handle_moved = true
+ return
+
+# Creates a popup for adding a shape at a position.
+func create_tag_context(pos: Vector2) -> ContextPopupType:
+ var btn_array: Array[Button] = []
+ for shape in ["path", "circle", "ellipse", "rect", "line"]:
+ var btn := Utils.create_btn(shape, add_tag_at_pos.bind(shape, pos),
+ false, SVGDB.get_tag_icon(shape))
+ btn.add_theme_font_override(&"font", load("res://visual/fonts/FontMono.ttf"))
+ btn_array.append(btn)
+ var tag_context := ContextPopup.instantiate()
+ add_child(tag_context)
+ tag_context.set_button_array(btn_array, true)
+ return tag_context
+
+func add_tag_at_pos(tag_name: String, pos: Vector2) -> void:
+ var tag: Tag
+ match tag_name:
+ "path": tag = TagPath.new(pos)
+ "circle": tag = TagCircle.new(pos)
+ "ellipse": tag = TagEllipse.new(pos)
+ "rect": tag = TagRect.new(pos)
+ "line": tag = TagLine.new(pos)
+ SVG.root_tag.add_tag(tag, PackedInt32Array([SVG.root_tag.get_child_count()]))
diff --git a/src/ui_parts/import_warning_dialog.gd b/src/ui_parts/import_warning_dialog.gd
new file mode 100644
index 000000000..85dcd9e92
--- /dev/null
+++ b/src/ui_parts/import_warning_dialog.gd
@@ -0,0 +1,62 @@
+extends PanelContainer
+
+signal imported
+
+@onready var warnings_label: RichTextLabel = %WarningsLabel
+@onready var texture_preview: TextureRect = %TexturePreview
+@onready var ok_button: Button = %ButtonContainer/OKButton
+
+var imported_text := ""
+
+func _ready() -> void:
+ ok_button.grab_focus()
+ # Convert forward and backward to show how GodSVG would display the given SVG.
+ var imported_text_parse_result := SVGParser.text_to_svg(imported_text)
+ var preview_text := SVGParser.svg_to_text(imported_text_parse_result.svg)
+ var preview_parse_result := SVGParser.text_to_svg(preview_text)
+ var preview := preview_parse_result.svg
+ if preview != null:
+ var scaling_factor := texture_preview.size.x * 2 / maxf(preview.width, preview.height)
+ var img := Image.new()
+ img.load_svg_from_string(SVGParser.svg_to_text(preview), scaling_factor)
+ if not img.is_empty():
+ img.fix_alpha_edges()
+ texture_preview.texture = ImageTexture.create_from_image(img)
+
+ var warnings := get_svg_errors(imported_text_parse_result)
+ if warnings.is_empty():
+ imported.emit()
+ for warning in warnings:
+ warnings_label.text += warning + "\n"
+
+
+func set_svg(text: String) -> void:
+ imported_text = text
+
+
+func get_svg_errors(parse_result: SVGParser.ParseResult) -> Array[String]:
+ var warnings: Array[String] = []
+ if parse_result.error != SVGParser.ParseError.OK:
+ warnings = [tr(&"#syntax_error") + ": " +\
+ tr(SVGParser.get_error_stringname(parse_result.error))]
+ else:
+ var svg_tag := parse_result.svg
+ var tids := svg_tag.get_all_tids()
+ for tid in tids:
+ var tag := svg_tag.get_tag(tid)
+ if tag is TagUnknown:
+ warnings.append(tr(&"#unknown_tag") + ": " + tag.name)
+ else:
+ for unknown_attrib in tag.unknown_attributes:
+ warnings.append(tr(&"#unknown_attribute") + ": " + unknown_attrib.name)
+ return warnings
+
+
+func _on_cancel_button_pressed() -> void:
+ queue_free()
+
+func _on_ok_button_pressed() -> void:
+ imported.emit()
+
+func _on_imported() -> void:
+ queue_free()
diff --git a/src/ui_parts/import_warning_dialog.tscn b/src/ui_parts/import_warning_dialog.tscn
new file mode 100644
index 000000000..41029838e
--- /dev/null
+++ b/src/ui_parts/import_warning_dialog.tscn
@@ -0,0 +1,107 @@
+[gd_scene load_steps=6 format=3 uid="uid://bhskf8yrulqtj"]
+
+[ext_resource type="Script" path="res://src/ui_parts/import_warning_dialog.gd" id="1_1rv5w"]
+[ext_resource type="Shader" path="res://src/shaders/zoom_shader.gdshader" id="2_o24gk"]
+[ext_resource type="Texture2D" uid="uid://c68og6bsqt0lb" path="res://visual/icons/backgrounds/Checkerboard.svg" id="3_k3bec"]
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="4_rpfrk"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_774g4"]
+shader = ExtResource("2_o24gk")
+shader_parameter/uv_scale = 2.0
+
+[node name="ImportWarningPanel" type="PanelContainer"]
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -174.0
+offset_top = -111.0
+offset_right = 174.0
+offset_bottom = 111.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_1rv5w")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 8
+theme_override_constants/margin_bottom = 8
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "#import_problems"
+horizontal_alignment = 1
+
+[node name="TextureContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="Checkerboard" type="TextureRect" parent="MarginContainer/VBoxContainer/TextureContainer"]
+material = SubResource("ShaderMaterial_774g4")
+custom_minimum_size = Vector2(128, 128)
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+texture = ExtResource("3_k3bec")
+stretch_mode = 1
+
+[node name="TexturePreview" type="TextureRect" parent="MarginContainer/VBoxContainer/TextureContainer/Checkerboard"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+expand_mode = 2
+stretch_mode = 5
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/TextureContainer"]
+custom_minimum_size = Vector2(256, 128)
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_top = 6
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 6
+
+[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/TextureContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="WarningsLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/TextureContainer/MarginContainer/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_colors/default_color = Color(1, 0.4, 0.4, 1)
+theme_override_fonts/normal_font = ExtResource("4_rpfrk")
+fit_content = true
+scroll_active = false
+autowrap_mode = 0
+
+[node name="ButtonContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 12
+alignment = 1
+
+[node name="CancelButton" type="Button" parent="MarginContainer/VBoxContainer/ButtonContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+text = "#cancel"
+
+[node name="OKButton" type="Button" parent="MarginContainer/VBoxContainer/ButtonContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+text = "#import"
+
+[connection signal="imported" from="." to="." method="_on_imported"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/ButtonContainer/CancelButton" to="." method="_on_cancel_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/ButtonContainer/OKButton" to="." method="_on_ok_button_pressed"]
diff --git a/src/ui_parts/inspector.gd b/src/ui_parts/inspector.gd
new file mode 100644
index 000000000..025cb7c95
--- /dev/null
+++ b/src/ui_parts/inspector.gd
@@ -0,0 +1,49 @@
+extends VBoxContainer
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const TagEditor = preload("tag_editor.tscn")
+
+@onready var tags_container: VBoxContainer = %ScrollContainer/Tags
+@onready var svg_tag_editor: MarginContainer = $SVGTagEditor
+@onready var add_button: Button = $VBoxContainer/AddButton
+
+func _ready() -> void:
+ SVG.root_tag.tag_layout_changed.connect(full_rebuild)
+ SVG.root_tag.changed_unknown.connect(full_rebuild)
+ full_rebuild()
+
+
+func full_rebuild() -> void:
+ for node in tags_container.get_children():
+ node.queue_free()
+ # Only add the first level of tags, they will automatically add their children.
+ for tag_idx in SVG.root_tag.get_child_count():
+ var tag_editor := TagEditor.instantiate()
+ tag_editor.tag = SVG.root_tag.child_tags[tag_idx]
+ tag_editor.tid = PackedInt32Array([tag_idx])
+ tags_container.add_child(tag_editor)
+
+func add_tag(tag_name: String) -> void:
+ var new_tid := PackedInt32Array([SVG.root_tag.get_child_count()])
+ var new_tag: Tag
+ match tag_name:
+ "circle": new_tag = TagCircle.new()
+ "ellipse": new_tag = TagEllipse.new()
+ "rect": new_tag = TagRect.new()
+ "path": new_tag = TagPath.new()
+ "line": new_tag = TagLine.new()
+ SVG.root_tag.add_tag(new_tag, new_tid)
+
+
+func _on_add_button_pressed() -> void:
+ var btn_array: Array[Button] = []
+ for tag_name in ["path", "circle", "ellipse", "rect", "line"]:
+ var btn := Utils.create_btn(tag_name, add_tag.bind(tag_name), false,
+ SVGDB.get_tag_icon(tag_name))
+ btn.add_theme_font_override(&"font", load("res://visual/fonts/FontMono.ttf"))
+ btn_array.append(btn)
+
+ var add_popup := ContextPopup.instantiate()
+ add_child(add_popup)
+ add_popup.set_button_array(btn_array, true, add_button.size.x)
+ Utils.popup_under_rect(add_popup, add_button.get_global_rect(), get_viewport())
diff --git a/src/ui_parts/inspector.tscn b/src/ui_parts/inspector.tscn
new file mode 100644
index 000000000..ccc58bb5f
--- /dev/null
+++ b/src/ui_parts/inspector.tscn
@@ -0,0 +1,75 @@
+[gd_scene load_steps=7 format=3 uid="uid://ccynisiuyn5qn"]
+
+[ext_resource type="Script" path="res://src/ui_parts/inspector.gd" id="1_16ggy"]
+[ext_resource type="PackedScene" uid="uid://bktmk76u7dsu0" path="res://src/ui_parts/root_tag_editor.tscn" id="2_jnl50"]
+[ext_resource type="Texture2D" uid="uid://eif2ioi0mw17" path="res://visual/icons/Plus.svg" id="3_vo6hf"]
+[ext_resource type="Script" path="res://src/ui_parts/tag_container.gd" id="4_i4hc2"]
+[ext_resource type="Script" path="res://src/ui_parts/move_to_overlay.gd" id="5_otlmf"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4j4hv"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.0980392, 0.0980392, 0.14902, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="Inspector" type="VBoxContainer"]
+custom_minimum_size = Vector2(408, 0)
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_right = 392.0
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 6
+script = ExtResource("1_16ggy")
+
+[node name="SVGTagEditor" parent="." instance=ExtResource("2_jnl50")]
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="AddButton" type="Button" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 0
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_override_constants/h_separation = 4
+text = "#add_tag"
+icon = ExtResource("3_vo6hf")
+
+[node name="TagContainer" type="PanelContainer" parent="VBoxContainer"]
+clip_contents = true
+custom_minimum_size = Vector2(0, 200)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_4j4hv")
+script = ExtResource("4_i4hc2")
+
+[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/TagContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+horizontal_scroll_mode = 0
+
+[node name="Tags" type="VBoxContainer" parent="VBoxContainer/TagContainer/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 5
+
+[node name="MoveToOverlay" type="Control" parent="VBoxContainer/TagContainer"]
+visible = false
+layout_mode = 2
+script = ExtResource("5_otlmf")
+
+[connection signal="pressed" from="VBoxContainer/AddButton" to="." method="_on_add_button_pressed"]
diff --git a/src/ui_parts/main_scene.tscn b/src/ui_parts/main_scene.tscn
new file mode 100644
index 000000000..0122c1954
--- /dev/null
+++ b/src/ui_parts/main_scene.tscn
@@ -0,0 +1,52 @@
+[gd_scene load_steps=6 format=3 uid="uid://ce6j54x27pom"]
+
+[ext_resource type="PackedScene" uid="uid://cr1fdlmbknnko" path="res://src/ui_parts/code_editor.tscn" id="1_0jgh3"]
+[ext_resource type="Texture2D" uid="uid://co75w07yqmcro" path="res://visual/icons/theme/SplitGrabber2.svg" id="1_7y812"]
+[ext_resource type="PackedScene" uid="uid://ccynisiuyn5qn" path="res://src/ui_parts/inspector.tscn" id="1_afxvd"]
+[ext_resource type="PackedScene" uid="uid://bvrncl7e6yn5b" path="res://src/ui_parts/display.tscn" id="3_qbqbs"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_082e3"]
+content_margin_left = 6.0
+content_margin_top = 6.0
+content_margin_right = 0.0
+content_margin_bottom = 6.0
+bg_color = Color(0.0235294, 0.0235294, 0.0784314, 1)
+border_color = Color(0.278431, 0.286275, 0.568627, 1)
+expand_margin_right = 6.0
+
+[node name="MainScene" type="HBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 0
+
+[node name="HSplitContainer" type="HSplitContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 6
+theme_override_icons/grabber = ExtResource("1_7y812")
+split_offset = -122
+
+[node name="PanelContainer" type="PanelContainer" parent="HSplitContainer"]
+custom_minimum_size = Vector2(360, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_082e3")
+
+[node name="MainContainer" type="VSplitContainer" parent="HSplitContainer/PanelContainer"]
+layout_mode = 2
+theme_override_constants/separation = 8
+split_offset = -400
+
+[node name="CodeEditor" parent="HSplitContainer/PanelContainer/MainContainer" instance=ExtResource("1_0jgh3")]
+layout_mode = 2
+
+[node name="Inspector" parent="HSplitContainer/PanelContainer/MainContainer" instance=ExtResource("1_afxvd")]
+layout_mode = 2
+
+[node name="Display" parent="HSplitContainer" instance=ExtResource("3_qbqbs")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
diff --git a/src/ui_parts/move_to_overlay.gd b/src/ui_parts/move_to_overlay.gd
new file mode 100644
index 000000000..4283a17b4
--- /dev/null
+++ b/src/ui_parts/move_to_overlay.gd
@@ -0,0 +1,16 @@
+extends Control
+
+# Runs every time the mouse moves. Returning true means you can drop the TIDs.
+func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
+ if not data is Array[PackedInt32Array]:
+ return false
+ get_parent().update_proposed_tid()
+ for tid in data:
+ if Utils.is_tid_parent(tid, Indications.proposed_drop_tid):
+ return false
+ return true
+
+# Runs when you drop the TIDs.
+func _drop_data(_at_position: Vector2, data: Variant) -> void:
+ if data is Array[PackedInt32Array]:
+ SVG.root_tag.move_tags_to(data, Indications.proposed_drop_tid)
diff --git a/src/ui_parts/palette_config.gd b/src/ui_parts/palette_config.gd
new file mode 100644
index 000000000..2af11c15b
--- /dev/null
+++ b/src/ui_parts/palette_config.gd
@@ -0,0 +1,158 @@
+extends PanelContainer
+
+const ColorSwatch = preload("res://src/ui_elements/color_swatch.tscn")
+const ConfigurePopup = preload("res://src/ui_parts/configure_color_popup.tscn")
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+
+signal color_picked(color: String)
+signal layout_changed
+
+var current_palette: ColorPalette
+var currently_edited_color: NamedColor
+
+@onready var margin_container: MarginContainer = $MarginContainer
+@onready var palette_label: Label = %MainContainer/HBoxContainer/PaletteLabel
+@onready var name_edit: BetterLineEdit = %MainContainer/HBoxContainer/NameEdit
+@onready var name_edit_button: Button = %MainContainer/HBoxContainer/EditButton
+@onready var colors_container: HFlowContainer = %MainContainer/ColorsContainer
+@onready var action_button: Button = $MarginContainer/HBoxContainer/ActionButton
+
+# Used to setup a palette for this element.
+func assign_palette(palette: ColorPalette) -> void:
+ current_palette = palette
+ rebuild_colors()
+
+# Rebuilds the content of a container.
+func rebuild_colors() -> void:
+ for child in colors_container.get_children():
+ child.queue_free()
+
+ set_label_text(current_palette.name)
+ if current_palette.name.is_empty():
+ popup_edit_name()
+ else:
+ set_label_text(current_palette.name)
+ for named_color in current_palette.named_colors:
+ var swatch := ColorSwatch.instantiate()
+ swatch.named_color = named_color
+ swatch.type = swatch.Type.CONFIGURE_COLOR
+ swatch.pressed.connect(popup_configure_color.bind(swatch))
+ colors_container.add_child(swatch)
+ if named_color == currently_edited_color:
+ # If you add a color, after the rebuild you should instantly edit the new color.
+ # TODO figure out how to do without waiting a frame.
+ await get_tree().process_frame
+ popup_configure_color(swatch)
+ # Add the add button.
+ var fake_swatch := ColorSwatch.instantiate()
+ fake_swatch.type = fake_swatch.Type.ADD_COLOR
+ fake_swatch.pressed.connect(popup_add_color)
+ colors_container.add_child(fake_swatch)
+
+func popup_configure_color(swatch: Button) -> void:
+ var configure_popup := ConfigurePopup.instantiate()
+ configure_popup.named_color = swatch.named_color
+ add_child(configure_popup)
+ configure_popup.color_edit.value_changed.connect(swatch.change_color)
+ configure_popup.color_name_edit.text_submitted.connect(swatch.change_color_name)
+ configure_popup.color_deletion_requested.connect(delete_color.bind(swatch.named_color))
+ Utils.popup_under_rect_center(configure_popup, swatch.get_global_rect(),
+ get_viewport())
+
+func popup_edit_name() -> void:
+ palette_label.hide()
+ name_edit_button.hide()
+ name_edit.show()
+ name_edit.text = current_palette.name
+ name_edit.grab_focus()
+ name_edit.caret_column = name_edit.text.length()
+
+func hide_name_edit() -> void:
+ palette_label.show()
+ name_edit_button.show()
+ name_edit.hide()
+
+# Update text color to red if the name won't work (because it's a duplicate).
+func _on_name_edit_text_changed(new_text: String) -> void:
+ var names: Array[String] = []
+ for palette in GlobalSettings.get_palettes():
+ names.append(palette.name)
+ if new_text in names and new_text != current_palette.name:
+ name_edit.add_theme_color_override(&"font_color", Color(1.0, 0.6, 0.6))
+ else:
+ name_edit.add_theme_color_override(&"font_color", Color(0.6, 1.0, 0.6))
+
+func _on_name_edit_text_submitted(new_name: String) -> void:
+ new_name = new_name.strip_edges()
+ var names: Array[String] = []
+ for palette in GlobalSettings.get_palettes():
+ names.append(palette.name)
+
+ if not new_name.is_empty() and new_name != current_palette.name and\
+ not new_name in names:
+ current_palette.name = new_name
+ GlobalSettings.save_user_data()
+
+ set_label_text(current_palette.name)
+ hide_name_edit()
+
+func _on_name_edit_text_change_canceled() -> void:
+ hide_name_edit()
+
+func popup_add_color() -> void:
+ var new_color := NamedColor.new("none", "")
+ current_palette.named_colors.append(new_color)
+ currently_edited_color = new_color
+ rebuild_colors()
+
+func set_label_text(new_text: String) -> void:
+ if new_text.is_empty():
+ palette_label.text = tr(&"#unnamed")
+ palette_label.add_theme_color_override(&"font_color", Color(1.0, 0.5, 0.5))
+ else:
+ palette_label.text = new_text
+ palette_label.remove_theme_color_override(&"font_color")
+
+func delete_color(named_color: NamedColor) -> void:
+ current_palette.named_colors.erase(named_color) # I hope this works.
+ rebuild_colors()
+
+func delete(idx: int) -> void:
+ GlobalSettings.get_palettes().remove_at(idx)
+ GlobalSettings.save_user_data()
+ layout_changed.emit()
+
+func move_up(idx: int) -> void:
+ var palette: ColorPalette = GlobalSettings.get_palettes().pop_at(idx)
+ GlobalSettings.get_palettes().insert(idx - 1, palette)
+ GlobalSettings.save_user_data()
+ layout_changed.emit()
+
+func move_down(idx: int) -> void:
+ var palette: ColorPalette = GlobalSettings.get_palettes().pop_at(idx)
+ GlobalSettings.get_palettes().insert(idx + 1, palette)
+ GlobalSettings.save_user_data()
+ layout_changed.emit()
+
+
+func _on_action_button_pressed() -> void:
+ var palette_idx := -1
+ for idx in GlobalSettings.get_palettes().size():
+ if GlobalSettings.get_palettes()[idx].name == current_palette.name:
+ palette_idx = idx
+
+ var btn_arr: Array[Button] = [Utils.create_btn(tr(&"#delete"),
+ delete.bind(palette_idx), false, load("res://visual/icons/Delete.svg"))]
+
+ if palette_idx >= 1:
+ btn_arr.append(Utils.create_btn(tr(&"#move_up"), move_up.bind(palette_idx),
+ false, load("res://visual/icons/MoveUp.svg")))
+ if palette_idx < GlobalSettings.get_palettes().size() - 1:
+ btn_arr.append(Utils.create_btn(tr(&"#move_down"), move_down.bind(palette_idx),
+ false, load("res://visual/icons/MoveDown.svg")))
+
+ var context_popup := ContextPopup.instantiate()
+ add_child(context_popup)
+ context_popup.set_button_array(btn_arr, true)
+ Utils.popup_under_rect_center(context_popup, action_button.get_global_rect(),
+ get_viewport())
diff --git a/src/ui_parts/palette_config.tscn b/src/ui_parts/palette_config.tscn
new file mode 100644
index 000000000..673f5f927
--- /dev/null
+++ b/src/ui_parts/palette_config.tscn
@@ -0,0 +1,97 @@
+[gd_scene load_steps=7 format=3 uid="uid://c5cavwa2xdt0b"]
+
+[ext_resource type="Script" path="res://src/ui_parts/palette_config.gd" id="1_2fy8k"]
+[ext_resource type="Texture2D" uid="uid://dr2erka82g6j4" path="res://visual/icons/Edit.svg" id="2_35dgp"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterLineEdit.gd" id="2_coqnx"]
+[ext_resource type="FontFile" uid="uid://clpf84p1lfwlp" path="res://visual/fonts/Font.ttf" id="2_cttpy"]
+[ext_resource type="Texture2D" uid="uid://ccbta5q43jobk" path="res://visual/icons/More.svg" id="5_vjxeq"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lq3bn"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="PaletteConfig" type="PanelContainer"]
+offset_right = 21.0
+offset_bottom = 46.0
+size_flags_horizontal = 3
+size_flags_vertical = 0
+script = ExtResource("1_2fy8k")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 6
+theme_override_constants/margin_right = 6
+theme_override_constants/margin_bottom = 6
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="MainContainer" type="VBoxContainer" parent="MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 8
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/HBoxContainer/MainContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_constants/separation = 6
+
+[node name="Control" type="Control" parent="MarginContainer/HBoxContainer/MainContainer/HBoxContainer"]
+custom_minimum_size = Vector2(0, 24)
+layout_mode = 2
+
+[node name="PaletteLabel" type="Label" parent="MarginContainer/HBoxContainer/MainContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 6
+
+[node name="NameEdit" type="LineEdit" parent="MarginContainer/HBoxContainer/MainContainer/HBoxContainer"]
+visible = false
+custom_minimum_size = Vector2(120, 0)
+layout_mode = 2
+focus_mode = 1
+theme_override_fonts/font = ExtResource("2_cttpy")
+theme_override_styles/normal = SubResource("StyleBoxFlat_lq3bn")
+max_length = 30
+script = ExtResource("2_coqnx")
+
+[node name="EditButton" type="Button" parent="MarginContainer/HBoxContainer/MainContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+tooltip_text = "#edit_palette_name"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"FlatButton"
+icon = ExtResource("2_35dgp")
+
+[node name="ColorsContainer" type="HFlowContainer" parent="MarginContainer/HBoxContainer/MainContainer"]
+custom_minimum_size = Vector2(0, 22)
+layout_mode = 2
+size_flags_vertical = 4
+theme_override_constants/h_separation = 3
+
+[node name="ActionButton" type="Button" parent="MarginContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 4
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+icon = ExtResource("5_vjxeq")
+
+[connection signal="text_change_canceled" from="MarginContainer/HBoxContainer/MainContainer/HBoxContainer/NameEdit" to="." method="_on_name_edit_text_change_canceled"]
+[connection signal="text_changed" from="MarginContainer/HBoxContainer/MainContainer/HBoxContainer/NameEdit" to="." method="_on_name_edit_text_changed"]
+[connection signal="text_submitted" from="MarginContainer/HBoxContainer/MainContainer/HBoxContainer/NameEdit" to="." method="_on_name_edit_text_submitted"]
+[connection signal="pressed" from="MarginContainer/HBoxContainer/MainContainer/HBoxContainer/EditButton" to="." method="popup_edit_name"]
+[connection signal="pressed" from="MarginContainer/HBoxContainer/ActionButton" to="." method="_on_action_button_pressed"]
diff --git a/src/ui_parts/root_tag_editor.gd b/src/ui_parts/root_tag_editor.gd
new file mode 100644
index 000000000..e32336d41
--- /dev/null
+++ b/src/ui_parts/root_tag_editor.gd
@@ -0,0 +1,182 @@
+extends MarginContainer
+
+# So, about this editor. Width and height don't have default values, so they use NAN and
+# use NumberEdit, rather than NumberField. Viewbox is a list and it also doesn't have a
+# default value, and it also needs a coupling functionality, so it uses 4 NumberEdits.
+
+const NumberEditType = preload("res://src/ui_elements/number_edit.gd")
+
+const coupled_icon = preload("res://visual/icons/Coupled.svg")
+const decoupled_icon = preload("res://visual/icons/Decoupled.svg")
+
+@onready var couple_button: Button = $Edits/CoupleButton
+@onready var width_button: Button = $Edits/Size/Width/WidthButton
+@onready var height_button: Button = $Edits/Size/Height/HeightButton
+@onready var viewbox_button: Button = $Edits/Viewbox/ViewboxButton
+@onready var width_edit: NumberEditType = $Edits/Size/Width/WidthEdit
+@onready var height_edit: NumberEditType = $Edits/Size/Height/HeightEdit
+@onready var viewbox_edit_x: NumberEditType = $Edits/Viewbox/Rect/ViewboxEditX
+@onready var viewbox_edit_y: NumberEditType = $Edits/Viewbox/Rect/ViewboxEditY
+@onready var viewbox_edit_w: NumberEditType = $Edits/Viewbox/Rect/ViewboxEditW
+@onready var viewbox_edit_h: NumberEditType = $Edits/Viewbox/Rect/ViewboxEditH
+
+func _ready() -> void:
+ SVG.root_tag.resized.connect(_on_resized)
+ SVG.root_tag.changed_unknown.connect(_on_unknown_changed)
+ update_attributes(true)
+
+
+func _on_resized() -> void:
+ update_attributes()
+
+func update_attributes(configure_coupling := false) -> void:
+ if configure_coupling:
+ update_coupling_config()
+ width_edit.set_value(SVG.root_tag.width, false)
+ height_edit.set_value(SVG.root_tag.height, false)
+ viewbox_edit_x.set_value(SVG.root_tag.viewbox.position.x, false)
+ viewbox_edit_y.set_value(SVG.root_tag.viewbox.position.y, false)
+ viewbox_edit_w.set_value(SVG.root_tag.viewbox.size.x, false)
+ viewbox_edit_h.set_value(SVG.root_tag.viewbox.size.y, false)
+ update_editable()
+
+func _on_unknown_changed() -> void:
+ if GlobalSettings.save_data.viewbox_coupling and (SVG.root_tag.viewbox !=\
+ Rect2(Vector2.ZERO, SVG.root_tag.get_size())):
+ GlobalSettings.modify_save_data(&"viewbox_coupling", false)
+ update_attributes(true)
+
+
+func _on_couple_button_toggled(toggled_on: bool) -> void:
+ GlobalSettings.modify_save_data(&"viewbox_coupling", toggled_on)
+ update_coupling_config()
+
+func update_coupling_config() -> void:
+ if SVG.root_tag.attributes.width.get_value().is_empty() or\
+ SVG.root_tag.attributes.height.get_value().is_empty() or\
+ SVG.root_tag.attributes.viewBox.get_value().is_empty():
+ couple_button.disabled = true
+ couple_button.mouse_default_cursor_shape = Control.CURSOR_ARROW
+ couple_button.icon = coupled_icon
+ else:
+ couple_button.disabled = false
+ couple_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ var coupling_on := GlobalSettings.save_data.viewbox_coupling
+ couple_button.button_pressed = coupling_on
+ couple_button.icon = coupled_icon if coupling_on else decoupled_icon
+ if coupling_on:
+ SVG.root_tag.attributes.viewBox.set_list(PackedFloat32Array([
+ 0.0, 0.0, SVG.root_tag.width, SVG.root_tag.height]))
+ update_editable()
+
+
+func update_editable() -> void:
+ var is_width_valid := is_finite(SVG.root_tag.attributes.width.get_num())
+ var is_height_valid := is_finite(SVG.root_tag.attributes.height.get_num())
+ var is_viewbox_valid: bool = !SVG.root_tag.attributes.viewBox.get_value().is_empty()
+ var coupling_on := GlobalSettings.save_data.viewbox_coupling and\
+ not couple_button.disabled
+
+ width_button.set_pressed_no_signal(is_width_valid)
+ height_button.set_pressed_no_signal(is_height_valid)
+ viewbox_button.set_pressed_no_signal(is_viewbox_valid)
+
+ width_edit.editable = is_width_valid
+ height_edit.editable = is_height_valid
+ viewbox_edit_x.editable = is_viewbox_valid and not coupling_on
+ viewbox_edit_y.editable = is_viewbox_valid and not coupling_on
+ viewbox_edit_w.editable = is_viewbox_valid
+ viewbox_edit_h.editable = is_viewbox_valid
+
+
+func _on_width_edit_value_changed(new_value: float) -> void:
+ if is_finite(new_value) and SVG.root_tag.attributes.width.get_num() != new_value:
+ SVG.root_tag.width = new_value
+ if GlobalSettings.save_data.viewbox_coupling:
+ SVG.root_tag.attributes.width.set_num(new_value,
+ Attribute.SyncMode.NO_PROPAGATION)
+ SVG.root_tag.attributes.viewBox.set_list_element(2, new_value)
+ else:
+ SVG.root_tag.attributes.width.set_num(new_value)
+ else:
+ SVG.root_tag.attributes.width.set_num(SVG.root_tag.width, false)
+
+func _on_height_edit_value_changed(new_value: float) -> void:
+ if is_finite(new_value) and SVG.root_tag.attributes.height.get_num() != new_value:
+ SVG.root_tag.height = new_value
+ if GlobalSettings.save_data.viewbox_coupling:
+ SVG.root_tag.attributes.height.set_num(new_value,
+ Attribute.SyncMode.NO_PROPAGATION)
+ SVG.root_tag.attributes.viewBox.set_list_element(3, new_value)
+ else:
+ SVG.root_tag.attributes.height.set_num(new_value)
+ else:
+ SVG.root_tag.attributes.height.set_num(SVG.root_tag.height, false)
+
+func _on_viewbox_edit_x_value_changed(new_value: float) -> void:
+ if SVG.root_tag.attributes.viewBox.get_value() != null:
+ SVG.root_tag.viewbox.position.x = new_value
+ SVG.root_tag.attributes.viewBox.set_list_element(0, new_value)
+
+func _on_viewbox_edit_y_value_changed(new_value: float) -> void:
+ if SVG.root_tag.attributes.viewBox.get_value() != null:
+ SVG.root_tag.viewbox.position.y = new_value
+ SVG.root_tag.attributes.viewBox.set_list_element(1, new_value)
+
+func _on_viewbox_edit_w_value_changed(new_value: float) -> void:
+ if SVG.root_tag.attributes.viewBox.get_value() != null and\
+ SVG.root_tag.attributes.viewBox.get_list_element(2) != new_value:
+ SVG.root_tag.viewbox.size.x = new_value
+ if GlobalSettings.save_data.viewbox_coupling and\
+ not SVG.root_tag.attributes.width.get_value().is_empty():
+ SVG.root_tag.attributes.viewBox.set_list_element(2, new_value,
+ Attribute.SyncMode.NO_PROPAGATION)
+ SVG.root_tag.attributes.width.set_num(new_value)
+ else:
+ SVG.root_tag.attributes.viewBox.set_list_element(2, new_value)
+
+func _on_viewbox_edit_h_value_changed(new_value: float) -> void:
+ if SVG.root_tag.attributes.viewBox.get_value() != null and\
+ SVG.root_tag.attributes.viewBox.get_list_element(3) != new_value:
+ SVG.root_tag.viewbox.size.y = new_value
+ if GlobalSettings.save_data.viewbox_coupling and\
+ not SVG.root_tag.attributes.height.get_value().is_empty():
+ SVG.root_tag.attributes.viewBox.set_list_element(3, new_value,
+ Attribute.SyncMode.NO_PROPAGATION)
+ SVG.root_tag.attributes.height.set_num(new_value)
+ else:
+ SVG.root_tag.attributes.viewBox.set_list_element(3, new_value)
+
+func _on_width_button_toggled(toggled_on: bool) -> void:
+ if toggled_on:
+ SVG.root_tag.attributes.width.set_num(SVG.root_tag.width)
+ update_coupling_config()
+ else:
+ if SVG.root_tag.attributes.viewBox.get_list_size() == 4:
+ SVG.root_tag.attributes.width.set_num(NAN)
+ update_coupling_config()
+ else:
+ width_button.set_pressed_no_signal(true)
+
+func _on_height_button_toggled(toggled_on: bool) -> void:
+ if toggled_on:
+ SVG.root_tag.attributes.height.set_num(SVG.root_tag.height)
+ update_coupling_config()
+ else:
+ if SVG.root_tag.attributes.viewBox.get_list_size() == 4:
+ SVG.root_tag.attributes.height.set_num(NAN)
+ update_coupling_config()
+ else:
+ height_button.set_pressed_no_signal(true)
+
+func _on_viewbox_button_toggled(toggled_on: bool) -> void:
+ if toggled_on:
+ SVG.root_tag.attributes.viewBox.set_rect(SVG.root_tag.viewbox)
+ update_coupling_config()
+ else:
+ if is_finite(SVG.root_tag.attributes.width.get_num()) and\
+ is_finite(SVG.root_tag.attributes.height.get_num()):
+ SVG.root_tag.attributes.viewBox.set_value("")
+ update_coupling_config()
+ else:
+ viewbox_button.set_pressed_no_signal(true)
diff --git a/src/ui_parts/root_tag_editor.tscn b/src/ui_parts/root_tag_editor.tscn
new file mode 100644
index 000000000..5b4d41393
--- /dev/null
+++ b/src/ui_parts/root_tag_editor.tscn
@@ -0,0 +1,134 @@
+[gd_scene load_steps=7 format=3 uid="uid://bktmk76u7dsu0"]
+
+[ext_resource type="Script" path="res://src/ui_parts/root_tag_editor.gd" id="1_xgyg0"]
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="2_fm5sa"]
+[ext_resource type="PackedScene" uid="uid://dad7fkhmsooc6" path="res://src/ui_elements/number_edit.tscn" id="3_1gu7n"]
+[ext_resource type="Texture2D" uid="uid://bv4lcvienlyfa" path="res://visual/icons/Coupled.svg" id="3_yhfll"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterToggleButton.gd" id="4_7r848"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u8h0i"]
+bg_color = Color(0.866667, 0.933333, 1, 0.133333)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="SVGTagEditor" type="MarginContainer"]
+offset_right = 100.0
+offset_bottom = 35.0
+theme_override_constants/margin_left = 6
+theme_override_constants/margin_top = 6
+theme_override_constants/margin_right = 6
+theme_override_constants/margin_bottom = 6
+script = ExtResource("1_xgyg0")
+
+[node name="Edits" type="HBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="Size" type="HBoxContainer" parent="Edits"]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="Width" type="VBoxContainer" parent="Edits/Size"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="WidthButton" type="Button" parent="Edits/Size/Width"]
+layout_mode = 2
+size_flags_horizontal = 4
+focus_mode = 0
+theme_type_variation = &"TextButton"
+theme_override_fonts/font = ExtResource("2_fm5sa")
+theme_override_font_sizes/font_size = 12
+toggle_mode = true
+text = "width"
+script = ExtResource("4_7r848")
+hover_pressed_font_color = Color(1, 1, 1, 0.4)
+
+[node name="WidthEdit" parent="Edits/Size/Width" instance=ExtResource("3_1gu7n")]
+custom_minimum_size = Vector2(48, 22)
+layout_mode = 2
+allow_lower = false
+
+[node name="Height" type="VBoxContainer" parent="Edits/Size"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="HeightButton" type="Button" parent="Edits/Size/Height"]
+layout_mode = 2
+size_flags_horizontal = 4
+focus_mode = 0
+theme_type_variation = &"TextButton"
+theme_override_fonts/font = ExtResource("2_fm5sa")
+theme_override_font_sizes/font_size = 12
+toggle_mode = true
+text = "height"
+script = ExtResource("4_7r848")
+hover_pressed_font_color = Color(1, 1, 1, 0.4)
+
+[node name="HeightEdit" parent="Edits/Size/Height" instance=ExtResource("3_1gu7n")]
+custom_minimum_size = Vector2(48, 22)
+layout_mode = 2
+allow_lower = false
+
+[node name="CoupleButton" type="Button" parent="Edits"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+tooltip_text = "#couple_button_tooltip"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+toggle_mode = true
+icon = ExtResource("3_yhfll")
+script = ExtResource("4_7r848")
+hover_pressed_stylebox = SubResource("StyleBoxFlat_u8h0i")
+
+[node name="Viewbox" type="VBoxContainer" parent="Edits"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="ViewboxButton" type="Button" parent="Edits/Viewbox"]
+layout_mode = 2
+size_flags_horizontal = 4
+focus_mode = 0
+theme_type_variation = &"TextButton"
+theme_override_fonts/font = ExtResource("2_fm5sa")
+theme_override_font_sizes/font_size = 12
+toggle_mode = true
+text = "viewBox"
+script = ExtResource("4_7r848")
+hover_pressed_font_color = Color(1, 1, 1, 0.4)
+
+[node name="Rect" type="HBoxContainer" parent="Edits/Viewbox"]
+layout_mode = 2
+
+[node name="ViewboxEditX" parent="Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
+custom_minimum_size = Vector2(48, 22)
+layout_mode = 2
+
+[node name="ViewboxEditY" parent="Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
+custom_minimum_size = Vector2(48, 22)
+layout_mode = 2
+
+[node name="ViewboxEditW" parent="Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
+custom_minimum_size = Vector2(48, 22)
+layout_mode = 2
+allow_lower = false
+
+[node name="ViewboxEditH" parent="Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
+custom_minimum_size = Vector2(48, 22)
+layout_mode = 2
+allow_lower = false
+
+[connection signal="toggled" from="Edits/Size/Width/WidthButton" to="." method="_on_width_button_toggled"]
+[connection signal="value_changed" from="Edits/Size/Width/WidthEdit" to="." method="_on_width_edit_value_changed"]
+[connection signal="toggled" from="Edits/Size/Height/HeightButton" to="." method="_on_height_button_toggled"]
+[connection signal="value_changed" from="Edits/Size/Height/HeightEdit" to="." method="_on_height_edit_value_changed"]
+[connection signal="toggled" from="Edits/CoupleButton" to="." method="_on_couple_button_toggled"]
+[connection signal="toggled" from="Edits/Viewbox/ViewboxButton" to="." method="_on_viewbox_button_toggled"]
+[connection signal="value_changed" from="Edits/Viewbox/Rect/ViewboxEditX" to="." method="_on_viewbox_edit_x_value_changed"]
+[connection signal="value_changed" from="Edits/Viewbox/Rect/ViewboxEditY" to="." method="_on_viewbox_edit_y_value_changed"]
+[connection signal="value_changed" from="Edits/Viewbox/Rect/ViewboxEditW" to="." method="_on_viewbox_edit_w_value_changed"]
+[connection signal="value_changed" from="Edits/Viewbox/Rect/ViewboxEditH" to="." method="_on_viewbox_edit_h_value_changed"]
diff --git a/src/ui_parts/settings_menu.gd b/src/ui_parts/settings_menu.gd
new file mode 100644
index 000000000..7235b0998
--- /dev/null
+++ b/src/ui_parts/settings_menu.gd
@@ -0,0 +1,74 @@
+extends PanelContainer
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const PaletteConfigWidget = preload("res://src/ui_parts/palette_config.tscn")
+const plus_icon = preload("res://visual/icons/Plus.svg")
+
+@onready var lang_button: Button = %Language
+@onready var palette_container: VBoxContainer = %PaletteContainer
+@onready var wrap_mouse: CheckBox = %WrapMouse
+
+func _ready() -> void:
+ if not DisplayServer.has_feature(DisplayServer.FEATURE_MOUSE_WARP):
+ wrap_mouse.set_pressed_no_signal(false)
+ wrap_mouse.disabled = true
+
+ update_language_button()
+ rebuild_color_palettes()
+
+func _on_window_mode_pressed() -> void:
+ GlobalSettings.save_window_mode = not GlobalSettings.save_window_mode
+
+func _on_svg_pressed() -> void:
+ GlobalSettings.save_svg = not GlobalSettings.save_svg
+
+func _on_close_pressed() -> void:
+ queue_free()
+
+func _on_language_pressed() -> void:
+ var btn_arr: Array[Button] = []
+ for lang in TranslationServer.get_loaded_locales():
+ btn_arr.append(Utils.create_btn(
+ TranslationServer.get_locale_name(lang) + " (" + lang + ")",
+ _on_language_chosen.bind(lang)))
+ var lang_popup := ContextPopup.instantiate()
+ add_child(lang_popup)
+ lang_popup.set_button_array(btn_arr, true, lang_button.size.x)
+ Utils.popup_under_rect(lang_popup, lang_button.get_global_rect(), get_viewport())
+
+func _on_language_chosen(locale: String) -> void:
+ GlobalSettings.language = locale
+ update_language_button()
+
+func update_language_button() -> void:
+ lang_button.text = tr(&"#language") + ": " + TranslationServer.get_locale().to_upper()
+
+
+func add_palette() -> void:
+ for palette in GlobalSettings.get_palettes():
+ # If there's an unnamed pallete, don't add a new one (there'll be a name clash).
+ if palette.name.is_empty():
+ return
+
+ GlobalSettings.get_palettes().append(ColorPalette.new())
+ GlobalSettings.save_user_data()
+ rebuild_color_palettes()
+
+func rebuild_color_palettes() -> void:
+ for palette_config in palette_container.get_children():
+ palette_config.queue_free()
+
+ for palette in GlobalSettings.get_palettes():
+ var palette_config := PaletteConfigWidget.instantiate()
+ palette_container.add_child(palette_config)
+ palette_config.assign_palette(palette)
+ palette_config.layout_changed.connect(rebuild_color_palettes)
+ # Add the button for adding a new palette.
+ var add_palette_button := Button.new()
+ add_palette_button.theme_type_variation = &"TranslucentButton"
+ add_palette_button.icon = plus_icon
+ add_palette_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ add_palette_button.focus_mode = Control.FOCUS_NONE
+ add_palette_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ palette_container.add_child(add_palette_button)
+ add_palette_button.pressed.connect(add_palette)
diff --git a/src/ui_parts/settings_menu.tscn b/src/ui_parts/settings_menu.tscn
new file mode 100644
index 000000000..702295c88
--- /dev/null
+++ b/src/ui_parts/settings_menu.tscn
@@ -0,0 +1,110 @@
+[gd_scene load_steps=6 format=3 uid="uid://1rylg17uwltw"]
+
+[ext_resource type="Script" path="res://src/ui_parts/settings_menu.gd" id="1_1gf4m"]
+[ext_resource type="Texture2D" uid="uid://c528knojuxbw6" path="res://visual/icons/Languages.svg" id="2_ndyp7"]
+[ext_resource type="Script" path="res://src/ui_elements/BetterTabContainer.gd" id="3_vslgx"]
+[ext_resource type="PackedScene" path="res://src/ui_elements/setting_check_box.tscn" id="4_2qeh2"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hto7q"]
+content_margin_left = 8.0
+content_margin_top = 10.0
+content_margin_right = 8.0
+content_margin_bottom = 10.0
+bg_color = Color(0.005, 0.005, 0.05, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.203922, 0.254902, 0.4, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[node name="SettingsMenu" type="PanelContainer"]
+custom_minimum_size = Vector2(362, 256)
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -180.0
+offset_top = -128.0
+offset_right = 180.0
+offset_bottom = 128.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_hto7q")
+script = ExtResource("1_1gf4m")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="Language" type="Button" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 4
+focus_mode = 0
+mouse_default_cursor_shape = 2
+icon = ExtResource("2_ndyp7")
+
+[node name="TabContainer" type="TabContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+script = ExtResource("3_vslgx")
+
+[node name="#input_tab" type="ScrollContainer" parent="VBoxContainer/TabContainer"]
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/#input_tab"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 0
+
+[node name="InvertZoom" parent="VBoxContainer/TabContainer/#input_tab/VBoxContainer" instance=ExtResource("4_2qeh2")]
+layout_mode = 2
+tooltip_text = "#invert_zoom_desc"
+text = "#invert_zoom"
+section_name = "input"
+setting_name = "invert_zoom"
+
+[node name="WrapMouse" parent="VBoxContainer/TabContainer/#input_tab/VBoxContainer" instance=ExtResource("4_2qeh2")]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "#wrap_mouse_desc"
+text = "#wrap_mouse"
+section_name = "input"
+setting_name = "wrap_mouse"
+
+[node name="UseCtrlForZoom" parent="VBoxContainer/TabContainer/#input_tab/VBoxContainer" instance=ExtResource("4_2qeh2")]
+layout_mode = 2
+tooltip_text = "#use_ctrl_for_zoom_desc"
+text = "#use_ctrl_for_zoom"
+section_name = "input"
+setting_name = "use_ctrl_for_zoom"
+
+[node name="#palettes" type="ScrollContainer" parent="VBoxContainer/TabContainer"]
+visible = false
+custom_minimum_size = Vector2(0, 120)
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="PaletteContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/#palettes"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 4
+
+[node name="Close" type="Button" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 8
+focus_mode = 0
+mouse_default_cursor_shape = 2
+text = "#close"
+
+[connection signal="pressed" from="VBoxContainer/Language" to="." method="_on_language_pressed"]
+[connection signal="pressed" from="VBoxContainer/Close" to="." method="_on_close_pressed"]
diff --git a/src/ui_parts/svg_file_dialog.gd b/src/ui_parts/svg_file_dialog.gd
new file mode 100644
index 000000000..311ce27c4
--- /dev/null
+++ b/src/ui_parts/svg_file_dialog.gd
@@ -0,0 +1,10 @@
+extends FileDialog
+
+func _on_file_selected(_path: String) -> void:
+ queue_free()
+
+func _on_canceled() -> void:
+ queue_free()
+
+func _on_confirmed() -> void:
+ queue_free()
diff --git a/src/ui_parts/svg_file_dialog.tscn b/src/ui_parts/svg_file_dialog.tscn
new file mode 100644
index 000000000..33b7957f7
--- /dev/null
+++ b/src/ui_parts/svg_file_dialog.tscn
@@ -0,0 +1,19 @@
+[gd_scene load_steps=2 format=3 uid="uid://bndmdmjlwqxfh"]
+
+[ext_resource type="Script" path="res://src/ui_parts/svg_file_dialog.gd" id="1_nw1bg"]
+
+[node name="SVGFileDialog" type="FileDialog"]
+title = "Open a File"
+position = Vector2i(160, 135)
+size = Vector2i(700, 400)
+visible = true
+ok_button_text = "#open"
+cancel_button_text = "#cancel"
+file_mode = 0
+access = 2
+filters = PackedStringArray("*.svg ; Scalable vector graphics")
+script = ExtResource("1_nw1bg")
+
+[connection signal="canceled" from="." to="." method="_on_canceled"]
+[connection signal="confirmed" from="." to="." method="_on_confirmed"]
+[connection signal="file_selected" from="." to="." method="_on_file_selected"]
diff --git a/src/ui_parts/tag_container.gd b/src/ui_parts/tag_container.gd
new file mode 100644
index 000000000..c0f887b94
--- /dev/null
+++ b/src/ui_parts/tag_container.gd
@@ -0,0 +1,94 @@
+extends PanelContainer
+
+# Autoscroll area on drag and drop. As a factor from edge to center.
+const autoscroll_frac = 0.35 # 35% of the screen will be taken by the autoscroll areas.
+const autoscroll_speed = 1500.0
+
+@onready var scroll_container: ScrollContainer = $ScrollContainer
+@onready var tags: VBoxContainer = %Tags
+@onready var covering_rect: Control = $MoveToOverlay
+
+func _process(delta: float) -> void:
+ if Indications.proposed_drop_tid.is_empty():
+ return
+
+ # Autoscroll when the dragged object is near the edge of the screen.
+ var full_area := scroll_container.get_global_rect()
+ var mouse_y := get_global_mouse_position().y
+ var center_y := full_area.get_center().y
+ # A factor in the range [-1, 1] for how far away the mouse is from the center.
+ var factor := (mouse_y - center_y) / (full_area.size.y / 2)
+ # Remap values from [0, 1] to [1 - autoscroll_area, 1].
+ var scroll_amount := maxf((absf(factor) - 1 + autoscroll_frac) / autoscroll_frac, 0)
+ # Increase autoscroll speed the closer to the edge of the container.
+ var scroll_value := int(delta * signf(factor) * scroll_amount * autoscroll_speed)
+ # Check if autoscrolling happened; if it did, the drop location may need updating.
+ var old_scroll_vertical := scroll_container.scroll_vertical
+ scroll_container.scroll_vertical += scroll_value
+ if scroll_container.scroll_vertical != old_scroll_vertical:
+ update_proposed_tid()
+
+func update_proposed_tid() -> void:
+ var y_pos := get_local_mouse_position().y + scroll_container.scroll_vertical
+ var in_top_buffer := false
+ var in_bottom_buffer := false
+ # Keep track of the last tag editor whose position is before y_pos.
+ var prev_tid := PackedInt32Array([-1])
+ var prev_y := -INF
+ # Keep track of the first tag editor whose end is after y_pos.
+ var next_tid := PackedInt32Array([SVG.root_tag.get_child_count()])
+ var next_y := INF
+
+ for tid in SVG.root_tag.get_all_tids():
+ var tag_rect := get_tag_editor_rect(tid)
+ var buffer := minf(tag_rect.size.y / 3, 26)
+ var tag_end := tag_rect.end.y
+ var tag_start := tag_rect.position.y
+ if y_pos < tag_end and tag_end < next_y:
+ next_y = tag_end
+ next_tid = tid
+ if y_pos > tag_end - buffer:
+ in_bottom_buffer = true
+ if y_pos > tag_start and tag_start > prev_y:
+ prev_y = tag_start
+ prev_tid = tid
+ if y_pos < tag_start + buffer:
+ in_top_buffer = true
+ # Set the proposed drop TID based on what the previous and next tag editors are.
+ if in_top_buffer:
+ Indications.set_proposed_drop_tid(prev_tid)
+ elif in_bottom_buffer:
+ Indications.set_proposed_drop_tid(Utils.get_parent_tid(next_tid) +\
+ PackedInt32Array([next_tid[-1] + 1]))
+ elif Utils.is_tid_parent_or_self(prev_tid, next_tid):
+ for i in range(prev_tid.size(), next_tid.size()):
+ if next_tid[i] != 0:
+ return
+ Indications.set_proposed_drop_tid(prev_tid + PackedInt32Array([0]))
+
+
+func _notification(what: int) -> void:
+ if is_inside_tree() and not get_tree().paused:
+ if what == NOTIFICATION_DRAG_BEGIN:
+ covering_rect.show()
+ update_proposed_tid()
+ elif what == NOTIFICATION_DRAG_END:
+ covering_rect.hide()
+ Indications.clear_proposed_drop_tid()
+
+func _gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and\
+ event.is_pressed() and not (event.ctrl_pressed or event.shift_pressed):
+ Indications.clear_all_selections()
+
+# This function assumes there exists a tag editor for the corresponding TID.
+func get_tag_editor_rect(tid: PackedInt32Array) -> Rect2:
+ if tid.is_empty():
+ return Rect2()
+
+ var tag_editor: Control = tags.get_child(tid[0])
+ for i in range(1, tid.size()):
+ tag_editor = tag_editor.child_tags_container.get_child(tid[i])
+ # Position relative to the tag container.
+ return Rect2(tag_editor.global_position - scroll_container.global_position +\
+ Vector2(0, scroll_container.scroll_vertical), tag_editor.size)
diff --git a/src/ui_parts/tag_editor.gd b/src/ui_parts/tag_editor.gd
new file mode 100644
index 000000000..b30b135d1
--- /dev/null
+++ b/src/ui_parts/tag_editor.gd
@@ -0,0 +1,241 @@
+extends VBoxContainer
+
+const ContextPopup = preload("res://src/ui_elements/context_popup.tscn")
+const TagEditor = preload("tag_editor.tscn")
+const TransformField = preload("res://src/ui_elements/transform_field.tscn")
+const NumberField = preload("res://src/ui_elements/number_field.tscn")
+const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
+const ColorField = preload("res://src/ui_elements/color_field.tscn")
+const PathField = preload("res://src/ui_elements/path_field.tscn")
+const EnumField = preload("res://src/ui_elements/enum_field.tscn")
+const UnknownField = preload("res://src/ui_elements/unknown_field.tscn")
+
+@onready var v_box_container: VBoxContainer = $Content/MainContainer
+@onready var attribute_container: VBoxContainer = %AttributeContainer
+var unknown_container: HFlowContainer # Only created if there are unknown attributes.
+@onready var paint_container: FlowContainer = %AttributeContainer/PaintAttributes
+@onready var shape_container: FlowContainer = %AttributeContainer/ShapeAttributes
+var child_tags_container: VBoxContainer # Only created if there are child tags.
+@onready var title_bar: PanelContainer = $Title
+@onready var content: PanelContainer = $Content
+@onready var title_icon: TextureRect = $Title/TitleBox/TitleIcon
+@onready var title_label: Label = $Title/TitleBox/TitleLabel
+@onready var title_button: Button = $Title/TitleBox/TitleButton
+
+var tid: PackedInt32Array
+var tag: Tag
+
+var surface := RenderingServer.canvas_item_create() # Used for the drop indicator.
+
+func _ready() -> void:
+ RenderingServer.canvas_item_set_parent(surface, get_canvas_item())
+ RenderingServer.canvas_item_set_z_index(surface, 1)
+ title_label.text = tag.name
+ Utils.set_max_text_width(title_label, 180.0, 0.0) # Handle TagUnknown gracefully.
+ title_icon.texture = tag.icon
+ Indications.selection_changed.connect(determine_selection_highlight)
+ Indications.hover_changed.connect(determine_selection_highlight)
+ Indications.proposed_drop_changed.connect(queue_redraw)
+ determine_selection_highlight()
+ # Fill up the containers. Start with unknown attributes, if there are any.
+ if not tag.unknown_attributes.is_empty():
+ unknown_container = HFlowContainer.new()
+ attribute_container.add_child(unknown_container)
+ attribute_container.move_child(unknown_container, 0)
+ for attribute in tag.unknown_attributes:
+ var input_field := UnknownField.instantiate()
+ input_field.attribute = attribute
+ input_field.attribute_name = attribute.name
+ unknown_container.add_child(input_field)
+ # Continue with supported attributes.
+ for attribute_key in tag.attributes:
+ var attribute: Attribute = tag.attributes[attribute_key]
+ var input_field: Control
+ if attribute is AttributeTransform:
+ input_field = TransformField.instantiate()
+ elif attribute is AttributeNumeric:
+ match attribute.mode:
+ AttributeNumeric.Mode.FLOAT:
+ input_field = NumberField.instantiate()
+ AttributeNumeric.Mode.UFLOAT:
+ input_field = NumberField.instantiate()
+ input_field.allow_lower = false
+ AttributeNumeric.Mode.NFLOAT:
+ input_field = NumberSlider.instantiate()
+ input_field.allow_lower = false
+ input_field.allow_higher = false
+ input_field.slider_step = 0.01
+ elif attribute is AttributeColor:
+ input_field = ColorField.instantiate()
+ elif attribute is AttributePath:
+ input_field = PathField.instantiate()
+ elif attribute is AttributeEnum:
+ input_field = EnumField.instantiate()
+ input_field.attribute = attribute
+ input_field.attribute_name = attribute_key
+ input_field.focused.connect(Indications.normal_select.bind(tid))
+ # Add the attribute to its corresponding container.
+ if attribute_key in tag.known_shape_attributes:
+ shape_container.add_child(input_field)
+ elif attribute_key in tag.known_inheritable_attributes:
+ paint_container.add_child(input_field)
+
+ if not tag.is_standalone():
+ child_tags_container = VBoxContainer.new()
+ v_box_container.add_child(child_tags_container)
+
+ for tag_idx in tag.get_child_count():
+ var child_tag := tag.child_tags[tag_idx]
+ var tag_editor := TagEditor.instantiate()
+ tag_editor.tag = child_tag
+ var new_tid := tid.duplicate()
+ new_tid.append(tag_idx)
+ tag_editor.tid = new_tid
+ child_tags_container.add_child(tag_editor)
+
+# Logic for dragging.
+func _get_drag_data(_at_position: Vector2) -> Variant:
+ var data: Array[PackedInt32Array] = Utils.filter_descendant_tids(
+ Indications.selected_tids.duplicate(true))
+ # Set up a preview.
+ var tags_container := VBoxContainer.new()
+ for drag_tid in data:
+ var preview := TagEditor.instantiate()
+ preview.tag = SVG.root_tag.get_tag(drag_tid)
+ preview.tid = drag_tid
+ preview.custom_minimum_size.x = size.x
+ preview.z_index = 2
+ tags_container.add_child(preview)
+ tags_container.modulate = Color(1, 1, 1, 0.85)
+ set_drag_preview(tags_container)
+ return data
+
+
+func _on_title_button_pressed() -> void:
+ var viewport := get_viewport()
+ var title_button_rect := title_button.get_global_rect()
+ Utils.popup_under_rect_center(Indications.get_selection_context(
+ Utils.popup_under_rect_center.bind(title_button_rect, viewport)),
+ title_button_rect, viewport)
+
+
+func _gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion and event.button_mask == 0:
+ if Indications.semi_hovered_tid != tid and\
+ not Utils.is_tid_parent(tid, Indications.hovered_tid):
+ Indications.set_hovered(tid)
+ elif event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_LEFT:
+ if event.is_pressed():
+ if event.shift_pressed:
+ Indications.shift_select(tid)
+ elif event.is_command_or_control_pressed():
+ Indications.ctrl_select(tid)
+ elif not tid in Indications.selected_tids:
+ Indications.normal_select(tid)
+ elif event.is_released() and not event.shift_pressed and not event.ctrl_pressed:
+ Indications.normal_select(tid)
+ accept_event()
+ elif event.button_index == MOUSE_BUTTON_RIGHT:
+ if not tid in Indications.selected_tids:
+ Indications.normal_select(tid)
+ var viewport := get_viewport()
+ var popup_pos := viewport.get_mouse_position()
+ Utils.popup_under_pos(Indications.get_selection_context(
+ Utils.popup_under_pos.bind(popup_pos, viewport)), popup_pos, viewport)
+ accept_event()
+
+func _on_mouse_exited() -> void:
+ Indications.remove_hovered(tid)
+ determine_selection_highlight()
+
+
+func determine_selection_highlight() -> void:
+ var title_sb := StyleBoxFlat.new()
+ title_sb.corner_radius_top_left = 4
+ title_sb.corner_radius_top_right = 4
+ title_sb.set_border_width_all(2)
+ title_sb.set_content_margin_all(4)
+
+ var content_sb := StyleBoxFlat.new()
+ content_sb.corner_radius_bottom_left = 4
+ content_sb.corner_radius_bottom_right = 4
+ content_sb.border_width_left = 2
+ content_sb.border_width_right = 2
+ content_sb.border_width_bottom = 2
+ content_sb.content_margin_top = 5
+ content_sb.content_margin_left = 7
+ content_sb.content_margin_bottom = 7
+ content_sb.content_margin_right = 7
+
+ var is_selected := tid in Indications.selected_tids
+ var is_hovered := Indications.hovered_tid == tid
+
+ if is_selected:
+ if is_hovered:
+ content_sb.bg_color = Color.from_hsv(0.625, 0.48, 0.27)
+ title_sb.bg_color = Color.from_hsv(0.625, 0.5, 0.38)
+ else:
+ content_sb.bg_color = Color.from_hsv(0.625, 0.5, 0.25)
+ title_sb.bg_color = Color.from_hsv(0.625, 0.6, 0.35)
+ content_sb.border_color = Color.from_hsv(0.6, 0.75, 0.75)
+ title_sb.border_color = Color.from_hsv(0.6, 0.75, 0.75)
+ elif is_hovered:
+ content_sb.bg_color = Color.from_hsv(0.625, 0.57, 0.19)
+ title_sb.bg_color = Color.from_hsv(0.625, 0.4, 0.2)
+ content_sb.border_color = Color.from_hsv(0.6, 0.55, 0.45)
+ title_sb.border_color = Color.from_hsv(0.6, 0.55, 0.45)
+ else:
+ content_sb.bg_color = Color.from_hsv(0.625, 0.6, 0.16)
+ title_sb.bg_color = Color.from_hsv(0.625, 0.45, 0.17)
+ content_sb.border_color = Color.from_hsv(0.6, 0.5, 0.35)
+ title_sb.border_color = Color.from_hsv(0.6, 0.5, 0.35)
+
+ var depth := tid.size() - 1
+ var depth_tint := depth * 0.12
+ if depth > 0:
+ content_sb.bg_color = Color.from_hsv(content_sb.bg_color.h + depth_tint,
+ content_sb.bg_color.s, content_sb.bg_color.v)
+ content_sb.border_color = Color.from_hsv(content_sb.border_color.h + depth_tint,
+ content_sb.border_color.s, content_sb.border_color.v)
+ title_sb.bg_color = Color.from_hsv(title_sb.bg_color.h + depth_tint,
+ title_sb.bg_color.s, title_sb.bg_color.v)
+ title_sb.border_color = Color.from_hsv(title_sb.border_color.h + depth_tint,
+ title_sb.border_color.s, title_sb.border_color.v)
+ content.add_theme_stylebox_override(&"panel", content_sb)
+ title_bar.add_theme_stylebox_override(&"panel", title_sb)
+
+# Draws the yellow indicator when drag-and-dropping tags.
+func _draw() -> void:
+ RenderingServer.canvas_item_clear(surface)
+
+ if Indications.proposed_drop_tid.is_empty():
+ return
+
+ for selected_tid in Indications.selected_tids:
+ if Utils.is_tid_parent_or_self(selected_tid, tid):
+ return
+
+ var parent_tid := Utils.get_parent_tid(tid)
+ # Draw the yellow indicator of drag and drop actions.
+ var drop_sb := StyleBoxFlat.new()
+ var proposed_drop_tid := Indications.proposed_drop_tid
+ drop_sb.border_color = Color.YELLOW
+ if proposed_drop_tid == parent_tid + PackedInt32Array([tid[-1]]):
+ drop_sb.border_width_top = 2
+ elif proposed_drop_tid == parent_tid + PackedInt32Array([tid[-1] + 1]):
+ drop_sb.border_width_bottom = 2
+ elif proposed_drop_tid == tid + PackedInt32Array([0]):
+ drop_sb.set_border_width_all(2)
+ if child_tags_container != null:
+ drop_sb.border_color = Color(Color.YELLOW, 0.4)
+ else:
+ return
+
+ drop_sb.draw_center = false
+ drop_sb.set_corner_radius_all(4)
+ drop_sb.draw(surface, Rect2(Vector2.ZERO, get_size()))
+
+# Block dragging from starting when pressing the title button.
+func _on_title_button_gui_input(event) -> void:
+ title_button.mouse_filter = Utils.mouse_filter_pass_non_drag_events(event)
diff --git a/src/ui_parts/tag_editor.tscn b/src/ui_parts/tag_editor.tscn
new file mode 100644
index 000000000..33efbb914
--- /dev/null
+++ b/src/ui_parts/tag_editor.tscn
@@ -0,0 +1,71 @@
+[gd_scene load_steps=4 format=3 uid="uid://cksx526iftj5d"]
+
+[ext_resource type="Script" path="res://src/ui_parts/tag_editor.gd" id="1_7i0c4"]
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="2_0lxvf"]
+[ext_resource type="Texture2D" uid="uid://cmepkbqde0jh0" path="res://visual/icons/SmallMore.svg" id="2_2n846"]
+
+[node name="TagEditor" type="VBoxContainer"]
+offset_left = 2.0
+offset_top = 2.0
+offset_right = 55.0
+offset_bottom = 32.0
+theme_override_constants/separation = 0
+script = ExtResource("1_7i0c4")
+
+[node name="Title" type="PanelContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="TitleBox" type="HBoxContainer" parent="Title"]
+layout_mode = 2
+theme_override_constants/separation = 6
+alignment = 1
+
+[node name="TitleIcon" type="TextureRect" parent="Title/TitleBox"]
+custom_minimum_size = Vector2(18, 18)
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 0
+mouse_filter = 2
+
+[node name="TitleLabel" type="Label" parent="Title/TitleBox"]
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_fonts/font = ExtResource("2_0lxvf")
+theme_override_font_sizes/font_size = 12
+horizontal_alignment = 1
+text_overrun_behavior = 3
+
+[node name="TitleButton" type="Button" parent="Title/TitleBox"]
+layout_mode = 2
+focus_mode = 0
+mouse_filter = 1
+mouse_default_cursor_shape = 2
+theme_type_variation = &"FlatButton"
+icon = ExtResource("2_2n846")
+
+[node name="Content" type="PanelContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="MainContainer" type="VBoxContainer" parent="Content"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 12
+
+[node name="AttributeContainer" type="VBoxContainer" parent="Content/MainContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="PaintAttributes" type="HFlowContainer" parent="Content/MainContainer/AttributeContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="ShapeAttributes" type="HFlowContainer" parent="Content/MainContainer/AttributeContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
+[connection signal="gui_input" from="Title/TitleBox/TitleButton" to="." method="_on_title_button_gui_input"]
+[connection signal="pressed" from="Title/TitleBox/TitleButton" to="." method="_on_title_button_pressed"]
diff --git a/src/ui_parts/view_camera.gd b/src/ui_parts/view_camera.gd
new file mode 100644
index 000000000..35759240e
--- /dev/null
+++ b/src/ui_parts/view_camera.gd
@@ -0,0 +1,78 @@
+extends Camera2D
+
+const default_font = preload("res://visual/fonts/Font.ttf")
+const axis_line_color = Color(0.5, 0.5, 0.5, 0.75)
+const major_grid_color = Color(0.5, 0.5, 0.5, 0.35)
+const minor_grid_color = Color(0.5, 0.5, 0.5, 0.15)
+const ticks_interval = 4
+
+var surface := RenderingServer.canvas_item_create() # Used for drawing the numbers.
+
+func _ready() -> void:
+ RenderingServer.canvas_item_set_parent(surface, get_canvas_item())
+ SVG.root_tag.resized.connect(queue_redraw)
+ Indications.zoom_changed.connect(change_zoom)
+ Indications.zoom_changed.connect(queue_redraw)
+
+func change_zoom() -> void:
+ zoom = Vector2(Indications.zoom, Indications.zoom)
+
+# Don't ask me to explain this.
+func _draw() -> void:
+ var size: Vector2 = Indications.viewport_size * 1.0 / zoom
+ draw_line(Vector2(-position.x, 0), Vector2(-position.x, size.y), axis_line_color)
+ draw_line(Vector2(0, -position.y), Vector2(size.x, -position.y), axis_line_color)
+
+ var major_points := PackedVector2Array()
+ var minor_points := PackedVector2Array()
+ var x_offset := fmod(-position.x, 1.0)
+ var y_offset := fmod(-position.y, 1.0)
+ var tick_distance := float(ticks_interval)
+ var zoom_level := zoom.x
+ var draw_minor_lines := zoom_level >= 3.0
+ var rate := nearest_po2(roundi(maxf(64.0 / (ticks_interval * zoom_level), 1.0)))
+
+ # The grid lines are always 1px wide, but the numbers need to be resized.
+ RenderingServer.canvas_item_clear(surface)
+ RenderingServer.canvas_item_set_transform(surface,
+ Transform2D(0, Vector2(1, 1) / zoom, 0, Vector2.ZERO))
+
+ var i := x_offset
+ while i <= size.x:
+ if fposmod(-position.x, tick_distance) != fposmod(i, tick_distance):
+ if draw_minor_lines:
+ minor_points.append(Vector2(i, 0))
+ minor_points.append(Vector2(i, size.y))
+ else:
+ var coord := snappedi(i + position.x, ticks_interval)
+ if int(float(coord) / ticks_interval) % rate == 0:
+ major_points.append(Vector2(i, 0))
+ major_points.append(Vector2(i, size.y))
+ default_font.draw_string(surface, Vector2(i * zoom_level + 4, 14),
+ String.num_int64(coord), HORIZONTAL_ALIGNMENT_LEFT, -1, 14,
+ axis_line_color)
+ elif coord % rate == 0:
+ minor_points.append(Vector2(i, 0))
+ minor_points.append(Vector2(i, size.y))
+ i += 1.0
+ i = y_offset
+ while i < size.y:
+ if fposmod(-position.y, tick_distance) != fposmod(i, tick_distance):
+ if draw_minor_lines:
+ minor_points.append(Vector2(0, i))
+ minor_points.append(Vector2(size.x, i))
+ else:
+ var coord := snappedi(i + position.y, ticks_interval)
+ if int(coord / float(ticks_interval)) % rate == 0:
+ major_points.append(Vector2(0, i))
+ major_points.append(Vector2(size.x, i))
+ default_font.draw_string(surface, Vector2(4, i * zoom_level + 14),
+ String.num_int64(coord), HORIZONTAL_ALIGNMENT_LEFT, -1, 14,
+ axis_line_color)
+ elif coord % rate == 0:
+ minor_points.append(Vector2(0, i))
+ minor_points.append(Vector2(size.x, i))
+ i += 1.0
+ if not major_points.is_empty():
+ draw_multiline(major_points, major_grid_color)
+ draw_multiline(minor_points, minor_grid_color)
diff --git a/src/ui_parts/viewport.gd b/src/ui_parts/viewport.gd
new file mode 100644
index 000000000..a200dbe89
--- /dev/null
+++ b/src/ui_parts/viewport.gd
@@ -0,0 +1,170 @@
+extends SubViewport
+
+const ZoomMenuType = preload("res://src/ui_parts/zoom_menu.gd")
+
+const buffer_view_space = 0.8
+const zoom_reset_buffer = 0.875
+
+# Holds zoom position for Ctrl + MMB zooming.
+var _zoom_to: Vector2
+
+@onready var display: TextureRect = %Checkerboard
+@onready var view: Camera2D = $ViewCamera
+@onready var controls: Control = %Checkerboard/Controls
+@onready var display_texture: TextureRect = %Checkerboard/DisplayTexture
+@onready var zoom_menu: ZoomMenuType = %ZoomMenu
+
+
+func _ready() -> void:
+ SVG.root_tag.resized.connect(resize)
+ Indications.viewport_size_changed.connect(adjust_view)
+ resize()
+ await get_tree().process_frame
+ zoom_menu.zoom_reset()
+
+# Top left corner.
+func set_view(new_position: Vector2) -> void:
+ var scaled_size := size / Indications.zoom
+ view.position = new_position.clamp(Vector2(view.limit_left, view.limit_top),
+ Vector2(view.limit_right, view.limit_bottom) - scaled_size)
+
+ var stripped_left := maxf(view.position.x, 0.0)
+ var stripped_top := maxf(view.position.y, 0.0)
+ var stripped_right := minf(view.position.x + scaled_size.x, SVG.root_tag.width)
+ var stripped_bottom := minf(view.position.y + scaled_size.y, SVG.root_tag.height)
+ display_texture.view_rect = Rect2(stripped_left, stripped_top,
+ stripped_right - stripped_left, stripped_bottom - stripped_top)
+ view.queue_redraw()
+
+
+# Adjust the SVG dimensions.
+func resize() -> void:
+ if SVG.root_tag.get_size().is_finite():
+ display.size = SVG.root_tag.get_size()
+ zoom_menu.zoom_reset()
+
+func center_frame() -> void:
+ var available_size := size * zoom_reset_buffer
+ var w_ratio := available_size.x / SVG.root_tag.width
+ var h_ratio := available_size.y / SVG.root_tag.height
+ zoom_menu.set_zoom(nearest_po2(ceili(minf(w_ratio, h_ratio) * 32)) / 64.0)
+ adjust_view()
+ set_view((SVG.root_tag.get_size() - size / Indications.zoom) / 2)
+
+
+func _unhandled_input(event: InputEvent) -> void:
+ if Indications.get_viewport().gui_is_dragging():
+ return
+
+ if event is InputEventMouseMotion and\
+ event.button_mask & (MOUSE_BUTTON_MASK_LEFT | MOUSE_BUTTON_MASK_MIDDLE):
+
+ # Zooming with Ctrl + MMB.
+ if event.ctrl_pressed and event.button_mask == MOUSE_BUTTON_MASK_MIDDLE:
+ if _zoom_to == Vector2.ZERO: # Set zoom position if starting action.
+ _zoom_to = get_mouse_position() / (size * 1.0)
+ zoom_menu.set_zoom(Indications.zoom * (1.0 +\
+ (1 if GlobalSettings.invert_zoom else -1) * (wrap_mouse(event.relative).y if\
+ GlobalSettings.wrap_mouse else event.relative.y) / 128.0), _zoom_to)
+ # Panning with LMB or MMB.
+ else:
+ set_view(view.position - (wrap_mouse(event.relative)\
+ if GlobalSettings.wrap_mouse else event.relative) / Indications.zoom)
+
+ elif event is InputEventPanGesture:
+
+ # Zooming with Ctrl + touch?
+ if event.ctrl_pressed:
+ zoom_menu.set_zoom(Indications.zoom * (1 + event.delta.y / 2))
+
+ # Panning with touch.
+ else:
+ set_view(view.position + event.delta * 32 / Indications.zoom)
+
+ # Zooming with touch.
+ elif event is InputEventMagnifyGesture:
+ zoom_menu.set_zoom(Indications.zoom * event.factor)
+
+ # Actions with scrolling.
+ elif event is InputEventMouseButton and event.is_pressed():
+ var move_vec := Vector2.ZERO
+ var zoom_dir := 0
+ var mouse_offset := get_mouse_position() / (size * 1.0)
+
+ # Zooming with scrolling.
+ if (not event.ctrl_pressed and not event.shift_pressed and\
+ not GlobalSettings.use_ctrl_for_zoom) or\
+ (event.ctrl_pressed and GlobalSettings.use_ctrl_for_zoom):
+ match event.button_index:
+ MOUSE_BUTTON_WHEEL_UP when GlobalSettings.invert_zoom: zoom_dir = -1
+ MOUSE_BUTTON_WHEEL_DOWN when GlobalSettings.invert_zoom: zoom_dir = 1
+ MOUSE_BUTTON_WHEEL_UP: zoom_dir = 1
+ MOUSE_BUTTON_WHEEL_DOWN: zoom_dir = -1
+
+ # Inverted panning with Shift + scrolling.
+ elif event.shift_pressed:
+ match event.button_index:
+ MOUSE_BUTTON_WHEEL_UP: move_vec = Vector2.LEFT
+ MOUSE_BUTTON_WHEEL_DOWN: move_vec = Vector2.RIGHT
+ MOUSE_BUTTON_WHEEL_LEFT: move_vec = Vector2.UP
+ MOUSE_BUTTON_WHEEL_RIGHT: move_vec = Vector2.DOWN
+
+ # Panning with scrolling.
+ else:
+ match event.button_index:
+ MOUSE_BUTTON_WHEEL_UP: move_vec = Vector2.UP
+ MOUSE_BUTTON_WHEEL_DOWN: move_vec = Vector2.DOWN
+ MOUSE_BUTTON_WHEEL_LEFT: move_vec = Vector2.LEFT
+ MOUSE_BUTTON_WHEEL_RIGHT: move_vec = Vector2.RIGHT
+
+ # Apply scroll data from above.
+ var factor: float = event.factor
+ if factor == roundf(factor): # Detects if precise factor is unsuported.
+ factor = 1.0
+ if zoom_dir == 1:
+ zoom_menu.zoom_in(factor, mouse_offset)
+ elif zoom_dir == -1:
+ zoom_menu.zoom_out(factor, mouse_offset)
+
+ set_view(view.position + move_vec * factor / Indications.zoom * 32)
+
+ else:
+ _zoom_to = Vector2.ZERO # Reset Ctrl + MMB zoom position if released.
+
+
+func _on_zoom_changed(new_zoom_level: float, offset: Vector2) -> void:
+ Indications.set_zoom(new_zoom_level)
+ adjust_view(offset)
+ display.material.set_shader_parameter(&"uv_scale",
+ nearest_po2(int(Indications.zoom * 32.0)) / 32.0)
+
+var last_size_adjusted := size / Indications.zoom
+func adjust_view(offset := Vector2(0.5, 0.5)) -> void:
+ var old_size := last_size_adjusted
+ last_size_adjusted = size / Indications.zoom
+
+ var zoomed_size := buffer_view_space * size / Indications.zoom
+ view.limit_left = int(-zoomed_size.x)
+ view.limit_right = int(zoomed_size.x + SVG.root_tag.width)
+ view.limit_top = int(-zoomed_size.y)
+ view.limit_bottom = int(zoomed_size.y + SVG.root_tag.height)
+ set_view(Vector2(lerpf(view.position.x,
+ view.position.x + old_size.x - size.x / Indications.zoom, offset.x),
+ lerpf(view.position.y, view.position.y + old_size.y - size.y / Indications.zoom,
+ offset.y)))
+
+func _on_size_changed() -> void:
+ Indications.set_viewport_size(size)
+
+func wrap_mouse(relative: Vector2) -> Vector2:
+ var view_rect := get_visible_rect().grow(-1.0)
+ var warp_margin := view_rect.size * 0.5
+ var mouse_pos := get_mouse_position()
+
+ if not view_rect.has_point(mouse_pos):
+ warp_mouse(Vector2(fposmod(mouse_pos.x, view_rect.size.x),
+ fposmod(mouse_pos.y, view_rect.size.y)))
+
+ return Vector2(fmod(relative.x + signf(relative.x) * warp_margin.x, view_rect.size.x),
+ fmod(relative.y + signf(relative.y) * warp_margin.y, view_rect.size.y)) -\
+ relative.sign() * warp_margin
diff --git a/src/ui_parts/zoom_menu.gd b/src/ui_parts/zoom_menu.gd
new file mode 100644
index 000000000..b0f296fed
--- /dev/null
+++ b/src/ui_parts/zoom_menu.gd
@@ -0,0 +1,59 @@
+extends HBoxContainer
+
+const MIN_ZOOM = 0.125
+const MAX_ZOOM = 512.0
+
+signal zoom_changed(zoom_level: float, offset: Vector2)
+signal zoom_reset_pressed
+
+@onready var zoom_out_button: Button = $ZoomOut
+@onready var zoom_in_button: Button = $ZoomIn
+@onready var zoom_reset_button: Button = $ZoomReset
+
+var _zoom_level: float
+
+
+func set_zoom(new_value: float, offset := Vector2(0.5, 0.5)) -> void:
+ new_value = clampf(new_value, MIN_ZOOM, MAX_ZOOM)
+ if _zoom_level != new_value:
+ _zoom_level = new_value
+ zoom_changed.emit(_zoom_level, offset)
+ update_buttons_appearance()
+
+func zoom_out(factor := 1.0, offset := Vector2(0.5, 0.5)) -> void:
+ if factor == 1.0:
+ set_zoom(_zoom_level / sqrt(2), offset)
+ else:
+ set_zoom(_zoom_level / (factor + 1), offset)
+
+func zoom_in(factor := 1.0, offset := Vector2(0.5, 0.5)) -> void:
+ if factor == 1.0:
+ set_zoom(_zoom_level * sqrt(2), offset)
+ else:
+ set_zoom(_zoom_level * 2 - _zoom_level / (factor + 1), offset)
+
+# This needs a custom implementation to whatever is listening to the signal.
+func zoom_reset() -> void:
+ zoom_reset_pressed.emit()
+
+
+func update_buttons_appearance() -> void:
+ if _zoom_level < 0.1:
+ zoom_reset_button.text = String.num(_zoom_level * 100, 2) + "%"
+ elif _zoom_level < 10.0:
+ zoom_reset_button.text = String.num(_zoom_level * 100, 1) + "%"
+ elif _zoom_level < 100.0:
+ zoom_reset_button.text = String.num_uint64(roundi(_zoom_level * 100)) + "%"
+ else:
+ zoom_reset_button.text = String.num(_zoom_level, 1) + "x"
+
+ var is_max_zoom := _zoom_level > MAX_ZOOM or is_equal_approx(_zoom_level, MAX_ZOOM)
+ var is_min_zoom := _zoom_level < MIN_ZOOM or is_equal_approx(_zoom_level, MIN_ZOOM)
+
+ zoom_in_button.disabled = is_max_zoom
+ zoom_in_button.mouse_default_cursor_shape = Control.CURSOR_ARROW if\
+ is_max_zoom else Control.CURSOR_POINTING_HAND
+
+ zoom_out_button.disabled = is_min_zoom
+ zoom_out_button.mouse_default_cursor_shape = Control.CURSOR_ARROW if\
+ is_min_zoom else Control.CURSOR_POINTING_HAND
diff --git a/src/ui_parts/zoom_menu.tscn b/src/ui_parts/zoom_menu.tscn
new file mode 100644
index 000000000..152c6da21
--- /dev/null
+++ b/src/ui_parts/zoom_menu.tscn
@@ -0,0 +1,72 @@
+[gd_scene load_steps=10 format=3 uid="uid://oltvrf01xrxl"]
+
+[ext_resource type="Texture2D" uid="uid://c2h5snkvemm4p" path="res://visual/icons/Minus.svg" id="1_8ggy2"]
+[ext_resource type="Script" path="res://src/ui_parts/zoom_menu.gd" id="1_18ab8"]
+[ext_resource type="Texture2D" uid="uid://eif2ioi0mw17" path="res://visual/icons/Plus.svg" id="2_284x5"]
+
+[sub_resource type="InputEventKey" id="InputEventKey_y2lqj"]
+device = -1
+ctrl_pressed = true
+keycode = 45
+unicode = 45
+
+[sub_resource type="Shortcut" id="Shortcut_ntgv0"]
+events = [SubResource("InputEventKey_y2lqj")]
+
+[sub_resource type="InputEventKey" id="InputEventKey_q17wx"]
+device = -1
+ctrl_pressed = true
+keycode = 48
+unicode = 48
+
+[sub_resource type="Shortcut" id="Shortcut_4v7wx"]
+events = [SubResource("InputEventKey_q17wx")]
+
+[sub_resource type="InputEventKey" id="InputEventKey_gqh1f"]
+device = -1
+ctrl_pressed = true
+keycode = 61
+unicode = 61
+
+[sub_resource type="Shortcut" id="Shortcut_y6ouu"]
+events = [SubResource("InputEventKey_gqh1f")]
+
+[node name="ZoomMenu" type="HBoxContainer"]
+offset_right = 114.0
+offset_bottom = 24.0
+alignment = 1
+script = ExtResource("1_18ab8")
+
+[node name="ZoomOut" type="Button" parent="."]
+layout_mode = 2
+tooltip_text = "#zoom_out"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+shortcut = SubResource("Shortcut_ntgv0")
+icon = ExtResource("1_8ggy2")
+icon_alignment = 1
+
+[node name="ZoomReset" type="Button" parent="."]
+custom_minimum_size = Vector2(58, 0)
+layout_mode = 2
+tooltip_text = "#zoom_reset"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_override_font_sizes/font_size = 13
+shortcut = SubResource("Shortcut_4v7wx")
+text = "100%"
+
+[node name="ZoomIn" type="Button" parent="."]
+layout_mode = 2
+tooltip_text = "#zoom_in"
+focus_mode = 0
+mouse_default_cursor_shape = 2
+theme_type_variation = &"IconButton"
+shortcut = SubResource("Shortcut_y6ouu")
+icon = ExtResource("2_284x5")
+icon_alignment = 1
+
+[connection signal="pressed" from="ZoomOut" to="." method="zoom_out"]
+[connection signal="pressed" from="ZoomReset" to="." method="zoom_reset"]
+[connection signal="pressed" from="ZoomIn" to="." method="zoom_in"]
diff --git a/translations/translation_sheet.csv b/translations/translation_sheet.csv
new file mode 100644
index 000000000..cc0063248
--- /dev/null
+++ b/translations/translation_sheet.csv
@@ -0,0 +1,105 @@
+,en,bg,de,ru_RU,uk_UA
+#language,Language,ŠŠ·ŠøŠŗ,Sprache, ŠÆŠ·ŃŠŗ,ŠŠ¾Š²Š°
+#invert_zoom,Invert zoom direction,ŠŠ±ŃŃŠ½Šø ŠæŠ¾ŃŠ¾ŠŗŠ°Ńа на ŃŠ²ŠµŠ»ŠøŃение,Zoomrichtung umkehren,ŠŠ½Š²ŠµŃŃŠøŃоваŃŃ Š½Š°ŠæŃŠ°Š²Š»ŠµŠ½ŠøŠµ маŃŃŃŠ°Š±ŠøŃованиŃ,ŠŠ½Š²ŠµŃŃŃŠ²Š°ŃŠø напŃŃŠ¼Š¾Šŗ маŃŃŃŠ°Š±ŃваннŃ
+#invert_zoom_desc,Swaps zoom in and zoom out with the mouse wheel.,Š Š°Š·Š¼ŠµŠ½Ń ŃŠ²ŠµŠ»ŠøŃŠµŠ½ŠøŠµŃŠ¾ Šø Š½Š°Š¼Š°Š»ŃŠ½ŠµŃо Ń ŠŗŠ¾Š»ŠµŠ»ŃŠµŃо на Š¼ŠøŃкаŃа.,Vertauscht Hinein- und Herauszoomen mit dem Mausrad.,ŠŠ¾Š¼ŠµŠ½ŃŠµŃ Š¼ŠµŃŃŠ°Š¼Šø ŃŠ²ŠµŠ»ŠøŃение Šø ŃŠ¼ŠµŠ½ŃŃŠµŠ½ŠøŠµ маŃŃŃŠ°Š±Š° ŃŠµŃез ŠŗŠ¾Š»ŠµŃико мŃŃŠø.,ŠŃнŃŃ Š¼ŃŃŃŃŠ¼Šø Š·Š±ŃŠ»ŃŃŠµŠ½Š½Ń Ń Š·Š¼ŠµŠ½ŃŠµŠ½Š½Ń маŃŃŃŠ°Š±ŃŠ²Š°Š½Š½Ń ŃŠµŃез колŃŃŠ°Ńко миŃŃ.
+#wrap_mouse,Wrap mouse,ŠŃевŃŃŃŠ°Š½Šµ на ŠŗŃŃŃŠ¾Ńа,Maus einschlieĆen,ŠŠ°Ń
Š²Š°ŃŠøŃŃ Š¼ŃŃŃ,ŠŠ°Ń
Š¾ŠæŠøŃŠø миŃŃ
+#wrap_mouse_desc,Wraps the mouse cursor around when panning the viewport.,ŠŃевŃŃŃŠ° ŠŗŃŃŃŠ¾Ńа ŠŗŠ¾Š³Š°Ńо ŃŠ¾Š¹ ГоŃŃŠøŠ³Š½Šµ ŠŗŃŠ°ŠøŃŠ°ŃŠ° на ŠµŠŗŃана.,SchlieĆt die Maus beim bewegen innerhalb des Fensters ein.,ŠŠøŃа Š±ŃŠ“ŠµŃ ŃŠµŠ»ŠµŠæŠ¾ŃŃŠøŃоваŃŃŃŃ Š¾Ń Š¾Š“Š½Š¾Š³Š¾ ŠŗŃŠ°Ń Го ŠæŃоŃивположного ŠæŃŠø ŠæŠµŃŠµŠ“вижении окна ŠæŃоŃмоŃŃŠ°.,ŠŠøŃа Š±ŃŠ“е ŃŠµŠ»ŠµŠæŠ¾ŃŃŃŠ²Š°ŃŠøŃŃ Š²ŃŠ“ оГного ŠŗŃŠ°Ń Го ŠæŃоŃилежного ŠæŃŠø ŠæŠµŃŠµŃŃŠ²Š°Š½Š½Ń Š²ŃŠŗŠ½Š° ŠæŠµŃŠµŠ³Š»ŃŠ“Ń.
+#use_ctrl_for_zoom,Use CTRL for zooming,ŠŠ·ŠæŠ¾Š»Š·Š²Š°Š¹ CTRL за ŃŠ²ŠµŠ»ŠøŃение,Strg-Taste zum Zoomen benutzen,ŠŃŠæŠ¾Š»ŃŠ·Š¾Š²Š°ŃŃ CTRL Š“Š»Ń Š¼Š°ŃŃŠ°Š±ŠøŃованиŃ,ŠŠøŠŗŠ¾ŃŠøŃŃŠ¾Š²ŃŠ²Š°ŃŠø CTRL Š“Š»Ń Š¼Š°ŃŃŃŠ°Š±ŃваннŃ.
+#use_ctrl_for_zoom_desc,"If turned on, scrolling will pan the view. To zoom, hold CTRL while scrolling.","ŠŠ¾Š³Š°Ńо е вклŃŃŠµŠ½Š¾, Š²Š»Š°ŃŠµŠ½ŠµŃо ŃŠµ ŠæŃŠµŠ¼ŠµŃŃŠø Š³Š»ŠµŠ“ŠŗŠ°ŃŠ°. ŠŠ° ŃŠ²ŠµŠ»ŠøŃение, Š½Š°ŃŠøŃни CTRL Š“Š¾ŠŗŠ°ŃŠ¾ Š²Š»Š°ŃŠøŃ","Wenn diese Option aktiviert ist, wird durch Scrollen die Ansicht geschwenkt. Halten Sie zum Zoomen beim Scrollen die Strg-Taste gedrückt.","ŠŃли вклŃŃŠµŠ½Š¾, ŠæŃŠ¾ŠŗŃŃŃŠŗŠ° Š±ŃŠ“ŠµŃ ŠæŠµŃŠµŠ¼ŠµŃаŃŃ Š¾ŠŗŠ½Š¾ ŠæŃŠ¾ŃмоŃŃŠ°. Š§ŃŠ¾Š±Ń маŃŃŠ°Š±ŠøŃоваŃŃ ŠŠ°Š¶Š¼ŠøŃе CTRL когГа ŠæŃокŃŃŃŠøŠ²Š°ŠµŃе.","ŠÆŠŗŃŠ¾ ŃŠ²Ńмкнено, гоŃŃŠ°Š½Š½Ń Š±ŃŠ“е ŃŃŃ
Š°ŃŠø Š²ŃŠŗŠ½Š¾ ŠæŠµŃŠµŠ³Š»ŃŠ“Ń. Щоб маŃŃŃŠ°Š±ŃŠ²Š°ŃŠø, Š·Š°ŃŠøŃнŃŃŃ CTRL Гоки гоŃŃŠ°ŃŃŠµ."
+#input_tab,Input,ŠŃ
оГни ŃŠøŠ³Š½Š°Š»Šø,Eingabe,Š£ŃŃŃŠ¾Š¹ŃŃŠ²Š° ввоГа,ŠŃŠøŃŃŃŠ¾Ń ввоГŃ
+#open,Open,ŠŃŠ²Š¾ŃŠø,Ćffnen,ŠŃŠŗŃŃŃŃ,ŠŃŠ“ŠŗŃŠøŃŠø
+#close,Close,ŠŠ°ŃŠ²Š¾ŃŠø,SchlieĆen,ŠŠ°ŠŗŃŃŃŃ,ŠŠ°ŠŗŃŠøŃŠø
+#repo_button_text,GodSVG Repository,Š ŠµŠæŠ¾Š·ŠøŃŠ¾ŃŠøŃŃŠ° на GodSVG,GodSVG Repository,Š ŠµŠæŠ¾Š·ŠøŃŠ¾Ńий GodSVG,Š ŠµŠæŠ¾Š·ŠøŃŠ¾ŃŃŠ¹ GodSVG
+#about_button_text,Aboutā¦,ŠŃŠ½Š¾ŃŠ½Š¾ ŠæŃŠøŠ»Š¾Š¶ŠµŠ½ŠøŠµŃоā¦,Ćberā¦,ŠŃо ŠæŃогŃаммŃā¦,ŠŃо Š“Š¾Š“Š°ŃŠ¾Šŗā¦
+#docs_button_text,Documentationā¦,ŠŠ¾ŠŗŃŠ¼ŠµŠ½ŃŠ°ŃŠøŃā¦,Dokumentationā¦,ŠŠ¾ŠŗŃŠ¼ŠµŠ½ŃŠ°ŃŠøŃā¦,ŠŠ¾ŠŗŃŠ¼ŠµŠ½ŃŠ°ŃŃŃā¦
+#donate_button_text,Donate (Github),ŠŠ°Ńение (Github),Spenden (Github),ДГелаŃŃ ŠæŠ¾Š¶ŠµŃŃŠ²Š¾Š²Š°Š½ŠøŠµ (Github),ŠŃŠ¾Š±ŠøŃŠø пожеŃŃŠ²ŃŠ²Š°Š½Š½Ń (Github)
+#zoom_in,Zoom In,Š£Š²ŠµŠ»ŠøŃŠø,Hineinzoomen,ŠŃиблизиŃŃ Š¼Š°ŃŃŠ°Š±,ŠŠ±ŃŠ»ŃŃŠøŃŠø маŃŃŃŠ°Š±
+#zoom_reset,Zoom Reset,РеŃŃŠ°ŃŃŠøŃай Š¼Š°Ńаба,Zoom zurücksetzen,ДкинŃŃŃ Š¼Š°ŃŃŠ°Š±,ДкинŃŃŠø маŃŃŃŠ°Š±
+#zoom_out,Zoom Out,ŠŠ°Š¼Š°Š»Šø,Herauszoomen,ŠŃГалиŃŃ Š¼Š°ŃŃŠ°Š±,ŠŠ¼ŠµŠ½ŃŠøŃŠø маŃŃŃŠ°Š±
+#settings,Settings,ŠŠ°ŃŃŃŠ¾Š¹ŠŗŠø,Einstellungen,ŠŠ°ŃŃŃŠ¾Š¹ŠŗŠø,ŠŠ°Š»Š°ŃŃŃŠ²Š°Š½Š½Ń
+#visuals,Visuals,ŠŃŠ°ŃŠøŠŗŠø,Visuelles,ŠŠ½ŠµŃний виГ,ŠŠ¾Š²Š½ŃŃŠ½Ńй Š²ŠøŠ³Š»ŃŠ“
+#show_grid,Show Grid,ŠŠ¾ŠŗŠ°Š¶Šø ŃŠµŃŠµŃŠŗŠ°Ńа,Raster anzeigen,ŠŠ¾ŠŗŠ°Š·Š°ŃŃ ŃŠµŃŠŗŃ,ŠŠ¾ŠŗŠ°Š·Š°ŃŠø ŅŃŠ°ŃŠŗŃ
+#show_handles,Show Handles,ŠŠ¾ŠŗŠ°Š¶Šø Š“ŃŃŠ¶ŠŗŠøŃе,Griffe anzeigen,ŠŠ¾ŠŗŠ°Š·Š°ŃŃ ŃŃŃŠŗŠø ŃŠµŠ“Š°ŠŗŃŠøŃованиŃ,ŠŠ¾ŠŗŠ°Š·Š°ŃŠø ŃŃŃŠŗŠø ŃŠµŠ“Š°Š³ŃŠ²Š°Š½Š½Ń
+#rasterize_svg,Rasterized SVG,РаŃŃŠµŃŠøŠ·ŠøŃŠ°Š¹ SVG,SVG rasterisieren,РаŃŃŠµŃизоваŃŃ SVG,РаŃŃŠµŃŠøŠ·ŃŠ²Š°ŃŠø SVG
+#enable_snap,Enable Snapping,ŠŠŗŠ»ŃŃŠø заŃ
Š²Š°ŃŠ°Š½ŠµŃо,Einrasten aktivieren,ŠŠŗŠ»ŃŃŠøŃŃ ŠæŃŠøŠ²ŃŠ·ŠŗŃ,ŠŠŗŃŠøŠ²ŃŠ²Š°ŃŠø ŠæŃŠøŠ»ŠøŠæŠ°Š½Š½Ń
+#snap_size,Snap size,Š Š°Š·Š¼ŠµŃ Š½Š° заŃ
Š²Š°ŃŠ°Š½ŠµŃо,EinrastgrƶĆe,Š Š°Š·Š¼ŠµŃ ŠæŃŠøŠ²ŃŠ·ŠŗŠø,РозмŃŃ ŠæŃŠøŠ»ŠøŠæŠ°Š½Š½Ń
+#copy_button_tooltip,Copy All Text,ŠŠ¾ŠæŠøŃай Š²ŃŠøŃŠŗŠøŃ ŃŠµŠŗŃŃ,Text kopieren,Š”ŠŗŠ¾ŠæŠøŃŠ¾Š²Š°ŃŃ Š²ŠµŃŃ ŃŠµŠŗŃŃ,ДкопŃŃŠ²Š°ŃŠø веŃŃ ŃŠµŠŗŃŃ
+#import_button_tooltip,Import SVG,ŠŠ¼ŠæŠ¾ŃŃ Š½Š° SVG,SVG importieren,ŠŠ¼ŠæŠ¾ŃŃŠøŃоваŃŃ SVG,ŠŠ¼ŠæŠ¾ŃŃŃŠ²Š°ŃŠø SVG
+#export_button_tooltip,Export,ŠŠŗŃпоŃŃ,Exportieren,ŠŠŗŃпоŃŃŠøŃоваŃŃ,ŠŠŗŃпоŃŃŃŠ²Š°ŃŠø
+#couple_button_tooltip,Couple/Decouple,ДвŃŃŠ¶Šø/ŠŃвŃŃŠ¶Šø,Koppeln/Entkoppeln,ŠŠ±ŃеГиниŃŃ/Š Š°Š·ŃŠµŠ“иниŃŃ,ŠāŃŠ“Š½Š°ŃŠø/РозāŃŠ“Š½Š°ŃŠø
+#duplicate,Duplicate,ŠŃŠ±Š»ŠøŃŠ°Š¹,Duplizieren,ŠŃŠ±Š»ŠøŃŠ¾Š²Š°ŃŃ,ŠŃŠ±Š»ŃŠ²Š°ŃŠø
+#move_up,Move Up,ŠŃемеŃŃŠø Š½Š°Š³Š¾ŃŠµ,Nach oben,ŠŠ¾Š“винŃŃŃ Š²Š²ŠµŃŃ
,ŠŠµŃеŃŃŠ½ŃŃŠø вгоŃŃ
+#move_down,Move Down,ŠŃемеŃŃŠø наГолŃ,Nach unten,ŠŠ¾Š“винŃŃŃ Š²Š½ŠøŠ·,ŠŠµŃеŃŃŠ½ŃŃŠø вниз
+#delete,Delete,ŠŃемаŃ
ни,Lƶschen,УГалиŃŃ,ŠŠøŠ“Š°Š»ŠøŃŠø
+#insert_before,Insert Before,ŠŠ¼Ńкни Š¾ŃзаГ,Davor einsetzen,,
+#insert_after,Insert After,ŠŠ¼Ńкни Š¾ŃŠæŃеГ,Danach einsetzen,ŠŃŃŠ°Š²ŠøŃŃ ŠæŠ¾ŃŠ»Šµ,ŠŃŃŠ°Š²ŠøŃŠø ŠæŃŃŠ»Ń
+#convert_to,Convert To,ŠŃевŃŃŠ½Šø в,Konvertieren zu,ŠŠ¾Š½Š²ŠµŃŃŠøŃоваŃŃ Š²,ŠŠ¾Š½Š²ŠµŃŃŃŠ²Š°ŃŠø в
+#err_not_svg,Doesnāt describe a SVG.,ТекŃŃŃŃ Š½Šµ Š¾ŠæŠøŃŠ²Š° SVG.,Beschreibt kein SVG.,ŠŠµ еŃŃŃ Š¾ŠæŠøŃŠ°Š½ŠøŠµŠ¼ SVG,ŠŠµ Ń Š¾ŠæŠøŃŠ¾Š¼ SVG.
+#err_improper_nesting,Improper nesting.,ŠŠµŃŃŠ²Š¼ŠµŃŃŠøŠ¼Šø ŃŠ°Š³Š¾Š²Šµ.,Ungültige Formatierung.,ŠŠµ ŠæŃŠ°Š²ŠøŠ»Ńное Š²ŠŗŠ»Š°Š“ŃŠ²Š°Š½ŠøŠµ.,ŠŠµŠæŃŠ°Š²ŠøŠ»ŃŠ½Šµ вклаГеннŃ.
+#shortcut_inspector_delete,Delete: Deletes the selected tags,Delete: ŠŠ·ŃŃŠøŠ²Š° ŠøŠ·Š±ŃŠ°Š½ŠøŃе ŃŠ°Š³Š¾Š²Šµ,Entf: AusgewƤhlte Elemente lƶschen,Delete: УГалиŃŃ Š²ŃŠµ Š²ŃŠ±ŃŠ°Š½Š½ŃŠµ ŃŃŠ³Šø,Delete: ŠŠøŠ“Š°Š»ŠøŃŠø ŃŃŃ Š¾Š±ŃŠ°Š½Ń ŃŠµŠ³Šø
+#shortcut_inspector_ctrl_down,Ctrl+Down: Moves the selected tags down,Ctrl+Down: ŠŃемеŃŃŠ²Š° ŠøŠ·Š±ŃŠ°Š½ŠøŃе ŃŠ°Š³Š¾Š²Šµ наГолŃ,Strg+Unten: AusgewƤhlte Elemente nach unten verschieben,Ctrl+Down: ŠŠ¾Š“винŃŃŃ Š²Š½ŠøŠ· Š²ŃŠµ Š²ŃŠ±ŃŠ°Š½Š½ŃŠµ ŃŃŠ³Šø,Ctrl+Down: ŠŠµŃеŃŃŠ½ŃŃŠø ŃŃŃ Š¾Š±ŃŠ°Š½Ń ŃŠµŠ³Šø вниз
+#shortcut_inspector_ctrl_up,Ctrl+Up: Moves the selected tags up,Ctrl+Up: ŠŃемеŃŃŠ²Š° ŠøŠ·Š±ŃŠ°Š½ŠøŃе ŃŠ°Š³Š¾Š²Šµ Š½Š°Š³Š¾ŃŠµ,Strg+Hoch: AusgewƤhlte Elemente nach oben verschieben,Ctrl+Up: ŠŠ¾Š“винŃŃŃ Š²Š²ŠµŃŃ
Š²ŃŠµ Š²ŃŠ±ŃŠ°Š½Š½ŃŠµ ŃŃŠ³Šø,Ctrl+Up: ŠŠµŃеŃŃŠ½ŃŃŠø ŃŃŃ Š¾Š±ŃŠ°Š½Ń ŃŠµŠ³Šø вгоŃŃ
+#shortcut_inspector_ctrl_d,Ctrl+D: Duplicates the selected tags,Ctrl+D: ŠŃŠ±Š»ŠøŃŠ°Š¹ ŠøŠ·Š±ŃŠ°Š½ŠøŃе ŃŠ°Š³Š¾Š²Šµ,Strg+D: AusgewƤhlte Elemente duplizieren,Ctrl+D: ŠŃŠ±Š»ŠøŃŠ¾Š²Š°ŃŃ Š²ŃŠ±ŃŠ°Š½Š½ŃŠµ ŃŃŠ³Šø,Ctrl+D: ŠŃŠ±Š»ŃŠ²Š°ŃŠø Š¾Š±ŃŠ°Š½Ń ŃŠµŠ³Šø
+#unknown_tag,Unknown Tag,ŠŠµŠæŠ¾Š·Š½Š°Ń ŃŠ°Š³,Unbekanntes Element,ŠŠµŠøŠ·Š²ŠµŃŃŠ½Ńй ŃŃŠ³,ŠŠµŠ²ŃГомий ŃŠµŠ³
+#unknown_attribute,Unknown Attribute,ŠŠµŠæŠ¾Š·Š½Š°Ń аŃŃŠøŠ±ŃŃ,Unbekannte Attribute,ŠŠµŠøŠ·Š²ŠµŃŃŠ½Ńй аŃŃŠøŠ±ŃŃ,ŠŠµŠ²ŃГомий аŃŃŠøŠ±ŃŃ
+#import,Import,ŠŠ¼ŠæŠ¾ŃŃŠøŃай,Importieren,ŠŠ¼ŠæŠ¾ŃŃŠøŃоваŃŃ,ŠŠ¼ŠæŠ¾ŃŃ
+#cancel,Cancel,ŠŃказ,Abbrechen,ŠŃмениŃŃ,ДкаŃŃŠ²Š°ŃŠø
+#import_problems,Import Problems,ŠŃоблеми в импоŃŃŠøŃŠ°Š½ŠµŃŠ¾,Probleme beim Importieren,ŠŃŠ¾Š±Š»ŠµŠ¼Ń Ń ŠøŠ¼ŠæŠ¾ŃŃŠøŃованием,ŠŃоблема Š· ŃŠ¼ŠæŠ¾ŃŃŃŠ²Š°Š½Š½Ńм
+#unknown_tooltip,GodSVG doesnāt recognize this attribute,GodSVG не ŃŠ°Š·ŠæŠ¾Š·Š½Š°Š²Š° ŃŠ¾Š·Šø аŃŃŠøŠ±ŃŃ,GodSVG erkennt diese Attribute nicht,GodSVG не Š¼Š¾Š¶ŠµŃ ŃŠ°ŃпознаŃŃ ŃŃŠ¾Ń аŃŃŠøŠ±ŃŃ,GodSVG не може ŃŠ¾Š·ŠæŃŠ·Š½Š°ŃŠø ŃŠµŠ¹ аŃŃŠøŠ±ŃŃ
+#export,Export,ŠŠŗŃпоŃŃŠøŃай,Exportieren,ŠŠŗŃпоŃŃ,ŠŠŗŃпоŃŃ
+#format,Format,Š¤Š¾ŃŠ¼Š°Ń,Format,Š¤Š¾ŃŠ¼Š°Ń,Š¤Š¾ŃŠ¼Š°Ń
+#scale,Scale,ŠŠ°Ńаб,Skalieren,ŠŠ°ŃŃŠ°Š±,ŠŠ°ŃŃŃŠ°Š±
+#size,Size,РазмеŃ,GrƶĆe,РазмеŃ,РозмŃŃ
+#final_size,Final size,ŠŃаен ŃŠ°Š·Š¼ŠµŃ,Endgültige GrƶĆe,Š¤ŠøŠ½Š°Š»ŃŠ½Ńй ŃŠ°Š·Š¼ŠµŃ,Š¤ŃŠ½Š°Š»Ńний ŃŠ¾Š·Š¼ŃŃ
+#export_configuration,Export Configuration,ŠŠ¾Š½ŃигŃŃŠ°ŃŠøŃ Š½Š° ŠµŠŗŃŠæŠ¾ŃŃŠ°,Export-Konfiguration,ŠŠ°ŃŃŃŠ¾Š¹ŠŗŠø ŃŠŗŃпоŃŃŠ°,ŠŠ°Š»Š°ŃŃŃŠ²Š°Š½Š½Ń ŠµŠŗŃŠæŠ¾ŃŃŃ
+#add_tag,Add new tag,ŠŠ¾Š±Š°Š²Šø нов ŃŠ°Š³,Neues Element hinzufügen,ŠŠ¾Š±Š°Š²ŠøŃŃ Š½Š¾Š²ŃŠ¹ ŃŃŠ³,ŠŠ¾Š“Š°ŃŠø новий ŃŠµŠ³
+#license_tab,License,ŠŠøŃенз,Lizenz,ŠŠøŃензиŃ,ŠŃŃŠµŠ½Š·ŃŃ
+#third-party-licenses_tab,Third-party licenses,ŠŠøŃензи Š¾Ń ŃŃŠµŃŠø паŃŃŠøŠø,Drittanbieter-Lizenzen,ŠŠøŃензии ŃŃŠµŃŃŠøŃ
лиŃ,ŠŃŃŠµŠ½Š·ŃŃ ŃŃŠµŃŃŃ
оŃŃŠ±
+#authors_tab,Authors,ŠŠ²ŃŠ¾ŃŠø,Autoren,ŠŠ²ŃŠ¾ŃŠø,ŠŠ²ŃŠ¾ŃŠø
+#kbd_shortcuts_tab,Shortcuts,ŠŃŃŠ·Šø ŠŗŠ»Š°Š²ŠøŃŠø,Tastenkombinationen,Š”Š¾ŃŠµŃŠ°Š½ŠøŃ ŠŗŠ»Š°Š²ŠøŃ,ŠŠ»Š°Š²ŃаŃŃŃŠ½Ń ŃŠŗŠ¾ŃŠ¾ŃŠµŠ½Š½Ń
+#palettes,Palettes,ŠŠ°Š»ŠµŃŠø,Paletten,ŠŠ°Š»ŠøŃŃŃ,ŠŠ°Š»ŃŃŃŠø
+#color_picker,Color Picker,Š¦Š²ŠµŃŠ½Š¾ колело,Farbauswahl,Š¦Š²ŠµŃŠ¾Š²Š°Ń ŠæŠøŠæŠµŃŠŗŠ°,ŠŠ¾Š»ŃŠ¾ŃŠ¾Š²Š° ŠæŃŠæŠµŃŠŗŠ°
+#add_color,Add color,ŠŠ¾Š±Š°Š²Šø ŃŠ²ŃŃ,Farbe hinzufügen,ŠŠ¾Š±Š°Š²ŠøŃŃ ŃŠ²ŠµŃ,ŠŠ¾Š“Š°ŃŠø колŃŃ
+#edit_palette_name,Edit palette name,ŠŃомени ŠøŠ¼ŠµŃо на ŠæŠ°Š»ŠµŃŠ°Ńа,Palettennamen Ƥndern,Š ŠµŠ“Š°ŠŗŃŠøŃоваŃŃ Š½Š°Š·Š²Š°Š½ŠøŠµ палиŃŃŃ,Š ŠµŠ“Š°Š³ŃŠ²Š°ŃŠø Š½Š°Š·Š²Ń ŠæŠ°Š»ŃŃŃŠø
+#unnamed,Unnamed,ŠŠµŠøŠ¼ŠµŠ½Ńван,Unbenannt,ŠŠµŠ· названиŃ,ŠŠµŠ· назви
+#edit_color_name,Edit color name,ŠŃомени ŠøŠ¼ŠµŃо на ŃŠ²ŠµŃа,Farbnamen Ƥndern,Š ŠµŠ“Š°ŠŗŃŠøŃоваŃŃ Š½Š°Š·Š²Š°Š½ŠøŠµ ŃŠ²ŠµŃа,Š ŠµŠ“Š°Š³ŃŠ²Š°ŃŠø Š½Š°Š·Š²Ń ŠŗŠ¾Š»ŃŠ¾ŃŃ
+#delete_color,Delete color,ŠŠ·ŃŃŠøŠ¹ ŃŠ²ŠµŃа,Farbe lƶschen,УГалиŃŃ ŃŠ²ŠµŃ,ŠŠøŠ“Š°Š»ŠøŃŠø колŃŃ
+#enable_color,Enable the color,ŠŠŗŠ»ŃŃŠø ŃŠ²ŠµŃа,Farbe aktivieren,ŠŠŗŃŠøŠ²ŠøŃŠ¾Š²Š°ŃŃ ŃŠ²ŠµŃ,ŠŠŗŃŠøŠ²ŃŠ²Š°ŃŠø колŃŃ
+#disable_color,Disable the color,ŠŠ·ŠŗŠ»ŃŃŠø ŃŠ²ŠµŃа,Farbe deaktivieren,ŠŠµŠ°ŠŗŃŠøŠ²ŠøŃŠ¾Š²Š°ŃŃ ŃŠ²ŠµŃ,ŠŠµŠ°ŠŗŃŠøŠ²ŃŠ²Š°ŃŠø колŃŃ
+#godot_third_party,Godot third-party components,ŠŠ¾Š¼ŠæŠ¾Š½ŠµŠ½ŃŠø в Godot Š¾Ń ŃŃŠµŃŠø Š»ŠøŃŠ°,Godot Drittanbieter-Komponente,ŠŠ¾Š¼ŠæŠ¾Š½ŠµŠ½ŃŃ Godot Š¾Ń ŃŃŠµŃŃŠøŃ
лиŃ,ŠŠ¾Š¼ŠæŠ¾Š½ŠµŠ½ŃŠø Godot Š²ŃŠ“ ŃŃŠµŃŃŃ
оŃŃŠ±
+#undo,Undo,ŠŃŃŠ½Šø назаГ,RückgƤngig,ŠŃмениŃŃ,ДкаŃŃŠ²Š°ŃŠø
+#redo,Redo,ŠŃŃŠ½Šø Š½Š°ŠæŃŠµŠ“,Wiederholen,ŠŠµŃнŃŃŃ,ŠŠ¾Š²ŠµŃнŃŃŠø
+#copy,Copy,ŠŠ¾ŠæŠøŃай,Kopieren,Š”ŠŗŠ¾ŠæŠøŃŠ¾Š²Š°ŃŃ,ДкопŃŃŠ²Š°ŃŠø
+#paste,Paste,ŠŠ¾ŃŃŠ°Š²Šø,Einfügen,ŠŃŃŠ°Š²ŠøŃŃ,ŠŃŃŠ°Š²ŠøŃŠø
+#cut,Cut,ŠŠ·Ńежи,Ausschneiden,ŠŃŃŠµŠ·Š°ŃŃ,ŠŠøŃŃŠ·Š°ŃŠø
+#alert,Alert!,ŠŃŠµŠ“ŃŠæŃежГение!,Warnung!,ŠŠ½ŠøŠ¼Š°Š½ŠøŠµ!,Увага!
+#file_open_fail_message,"The file couldn't be opened.\nTry checking the file path, ensure that the file is not deleted, or choose a different file.","ФайлŃŃ Š½Šµ може Га Š±ŃŠ“е Š¾ŃвоŃен.\nŠŠæŠøŃŠ°Š¹Ńе Га ŠæŃŠ¾Š²ŠµŃŠøŃе ŠæŃŃŃ ŠŗŃŠ¼ ŃŠ°Š¹Š»Š°, ŃŠ²ŠµŃŠµŃŠµ ŃŠµ, ŃŠµ ŃŠ°Š¹Š»Š° не е ŠøŠ·ŃŃŠøŃ, или ŠøŠ·Š±ŠµŃеŃе ŃŠ°Š·Š»ŠøŃен ŃŠ°Š¹Š».","Die Datei konnte nicht geƶffnet werden.\nĆberprüfe den Dateipfad, ob die Datei existiert, oder wƤhle eine andere Datei.","ŠŠµŠ²Š¾Š·Š¼Š¾Š¶Š½Š¾ Š¾ŃŠŗŃŃŃŃ ŃŃŠ¾Ń ŃŠ°Š¹Š».\nŠŠ¾ŠæŃŠ¾Š±ŃŠ¹Ńе ŠæŃŠ¾Š²ŠµŃŠøŃŃ ŠæŃŃŃ Š“Š¾ ŃŠ°Š¹Š»Š°, ŃŠ±ŠµŠ“ŠøŃŠµŃŃ ŃŃŠ¾ ŃŠ°Š¹Š» не ŃŠ“ален або Š²ŃŠ±ŠµŃŠøŃе Š“ŃŃŠ³Š¾Š¹ ŃŠ°Š¹Š».","ŠŠµŠ¼Š¾Š¶Š»ŠøŠ²Š¾ Š²ŃŠ“ŠŗŃŠøŃŠø ŃŠ°Š¹Š».\nŠ”ŠæŃŠ¾Š±ŃŠ¹ŃŠµ ŠæŠµŃŠµŠ²ŃŃŠøŃŠø ŃŠ»ŃŃ
Го ŃŠ°Š¹Š»Ń, впевнŃŃŃŃŃ ŃŠ¾ ŃŠ°Š¹Š» не виГалений або обеŃŃŃŃ ŃŠ½Ńий ŃŠ°Š¹Š»."
+#file_open_unsupported_extension,"""{passed_extension}"" is a unsupported file extension. Only ""svg"" files are supported.","""{passed_extension}"" не е поГГŃŃŠ¶Š°Š½ ŃŠ¾ŃмаŃ. ŠŠ“инŃŃŠ²ŠµŠ½Š¾ ""svg"" ŃŠ°Š¹Š»Š¾Š²ŠµŃе ŃŠ° поГŃŃŠ¶Š°Š½Šø.","""{passed_extension}"" ist eine nicht unterstützte Dateierweiterung. Nur ""svg"" Dateien werden unterstützt.","""{passed_extension}"" ŃŃŠ¾ ŃŠ°ŃŃŠøŃение ŃŠ°Š¹Š»Š° ŠŗŠ¾ŃŠ¾Ńое не ŠæŠ¾Š“Š“ŠµŃŠ¶ŠøŠ²Š°ŠµŃŃŃ. ŠŠ¾Š“Š“ŠµŃŠ¶ŠøŠ²Š°ŠµŃŃŃ ŃŠ¾Š»Ńко Ā«svgĀ».","""{passed_extension}"" ŃŠµ ŃŠ¾Š·ŃŠøŃŠµŠ½Š½Ń ŃŠ°Š¹Š»Ń ŃŠŗŠµ не ŠæŃŠ“ŃŃŠøŠ¼ŃŃŃŃŃŃ. ŠŃŠ“ŃŃŠøŠ¼ŃŃŃŃŃŃ ŃŃŠ»ŃŠŗŠø Ā«svgĀ» ŃŠ°Š¹Š»Šø."
+#file_open_empty_extension,"The file extension is empty. Only ""svg"" files are supported.","Š¤Š¾ŃŠ¼Š°ŃŃŃ Šµ ŠæŃŠ°Š·ŠµŠ½. ŠŠ“инŃŃŠ²ŠµŠ½Š¾ ""svg"" ŃŠ°Š¹Š»Š¾Š²ŠµŃе ŃŠ° поГŃŃŠ¶Š°Š½Šø.","Die Dateierweiterung ist leer. Nur ""svg""-Dateien werden unterstützt.",РаŃŃŠøŃение ŃŠ°Š¹Š»Š° ŠæŃŃŃŃŠ¾Šµ. ŠŠ¾Š“Š“ŠµŃŠ¶ŠøŠ²Š°ŠµŃŃŃ ŃŠ¾Š»Ńко Ā«svgĀ» ŃŠ°Š¹Š»Ń.,Š Š¾Š·ŃŠøŃŠµŠ½Š½Ń ŃŠ°Š¹Š»Ń ŠæŠ¾ŃŠ¾Š¶Š½Ń. ŠŃŠ“ŃŃŠøŠ¼ŃŃŃŃŃŃ ŃŃŠ»ŃŠŗŠø Ā«svgĀ» ŃŠ°Š¹Š»Šø.
+#ok,OK,ŠŠ¾Š±Ńе,OK,Š„Š¾ŃŠ¾Ńо,ŠŠ¾Š±Ńе
+#autoformatting_options,Autoformatting options,ŠŠæŃŠøŠø за Š°Š²ŃŠ¾Š¼Š°ŃŠøŃно ŃŠ¾ŃŠ¼Š°ŃŠøŃане,Autoformatierungsoptionen,ŠŠ°ŃŃŃŠ¾Š¹ŠŗŠø Š°Š²ŃŠ¾ŃŠ¾ŃŠ¼Š°ŃŠøŃŠ¾Š²Š°Š½ŠøŃ,ŠŠ°Š»Š°ŃŃŃŠ²Š°Š½Š½Ń Š°Š²ŃŠ¾ŃŠ¾ŃŠ¼Š°ŃŃŠ²Š°Š½Š½Ń
+#numbers,Numbers,Š§ŠøŃŠ»Š°,Nummern,ЦиŃŃŃ,ЦиŃŃŠø
+#colors,Colors,Š¦Š²ŠµŃŠ¾Š²Šµ,Farben,Š¦Š²ŠµŃŠ°,ŠŠ¾Š»ŃŠ¾ŃŠø
+#paths,Paths,ŠŃŃŠµŠŗŠø,Pfade,ŠŃŃŠø,ŠØŠ»ŃŃ
Šø
+#transforms,Transforms,Š¢ŃŠ°Š½ŃŃŠ¾ŃŠ¼Š°ŃŠøŠø,Transformationen,,
+#add_trailing_newline,Add trailing newline,ŠŠ¾Š±Š°Š²Šø нов ŃŠµŠ“ Š½Š°ŠŗŃŠ°Ń,Nachfolgenden Zeilenumbruch hinzufügen,,
+#use_shorthand_tag_syntax,Use shorthand tag syntax,ŠŠ·ŠæŠ¾Š»Š·Š²Š°Š¹ ŠŗŃŠ°ŃŃŠŗ ŃŠøŠ½ŃŠ°ŠŗŃŠøŃ за ŃŠ°Š³Š¾Š²Šµ,Kurzbezeichnung für Tags verwenden,,
+#enable_autoformatting,Enable autoformatting,Enable autoformatting,Autoformatierung einschalten,ŠŠŗŠ»ŃŃŠøŃŃ Š°Š²ŃŠ¾ŃŠ¾ŃŠ¼Š°ŃŠøŃŠ¾Š²Š°Š½ŠøŠµ,ŠŠŗŃŠøŠ²ŃŠ²Š°ŃŠø Š°Š²ŃŠ¾ŃŠ¾ŃŠ¼Š°ŃŃŠ²Š°Š½Š½Ń
+#remove_zero_padding,Remove zero padding,ŠŃемаŃ
ни Š½ŃлиŃе Š½Š°ŠŗŃаŃ,Nullenversatz entfernen,УГалиŃŃ Š·Š°ŠæŠ¾Š»Š½ŃŃŃŠøŠµ Š½ŃŠ»Šø,ŠŠøŠ“Š°Š»ŠøŃŠø заповнŃŃŃŠø Š½ŃŠ»Ń
+#remove_leading_zero,Remove leading zero,ŠŃемаŃ
ни Š²Š¾Š“ŠµŃŠ°Ńа Š½Ńла,Führende Null entfernen,УГалиŃŃ Š½Š°ŃŠ°Š»ŃŠ½ŃŠ¹ нолŃ,ŠŠøŠ“Š°Š»ŠøŃŠø ŠæŠ¾ŃŠ°Ńковий Š½ŃŠ»Ń
+#remove_plus_sign,Remove plus sign,ŠŃемаŃ
ни знака плŃŃ,Pluszeichen entfernen,УГалиŃŃ Š·Š½Š°Šŗ плŃŃŠ°,ŠŠøŠ“Š°Š»ŠøŃŠø знак плŃŃ
+#convert_rgb_to_hex,Convert rgb format to hex,ŠŃŠµŠ¾Š±ŃŠ°Š·Ńвай ŃŠ¾ŃмаŃŃŃ rgb в hex,RGB zu HEX konvertieren,ŠŠ¾Š½Š²ŠµŃŃŠøŃоваŃŃ rgb ŃŠ¾ŃŠ¼Š°Ń Š² hex,ŠŠ¾Š½Š²ŠµŃŃŃŠ²Š°ŃŠø rgb ŃŠ¾ŃŠ¼Š°Ń Ń hex
+#convert_named_to_hex,Convert named colors to hex,ŠŃŠµŠ¾Š±ŃŠ°Š·Ńвай ŠøŠ¼ŠµŠ½ŃваниŃе ŃŠ²ŠµŃове в hex,Benannte Farben zu HEX konvertieren,ŠŠ¾Š½Š²ŠµŃŃŠøŃоваŃŃ ŃŠ²ŠµŃа Ń Š½Š°Š·Š²Š°Š½ŠøŃŠ¼Šø в hex,ŠŠ¾Š½Š²ŠµŃŃŃŠ²Š°ŃŠø ŠŗŠ¾Š»ŃŠ¾ŃŠø Š· назвами Ń hex
+#use_shorthand_hex_code,Use shorthand hex code,ŠŠ·ŠæŠ¾Š»Š·Š²Š°Š¹ ŠŗŃŠ°ŃŃŠŗ hex коГ,Verkürzten HEX-Code verwenden,ŠŃŠæŠ¾Š»ŃŠ·Š¾Š²Š°ŃŃ ŠŗŠ¾ŃŠ¾Ńкие hex коГŃ,ŠŠøŠŗŠ¾ŃŠøŃŃŠ¾Š²ŃŠ²Š°ŃŠø ŃŠŗŠ¾ŃŠ¾ŃŠµŠ½Ń hex коГи
+#use_short_named_colors,Use short named colors,ŠŠ·ŠæŠ¾Š»Š·Š²Š°Š¹ ŠŗŃŠ°ŃŠŗŠø ŠøŠ¼ŠµŠ½ŃŠ²Š°Š½Šø ŃŠ²ŠµŃове,Verkürzte Farbnamen verwenden,ŠŃŠæŠ¾Š»ŃŠ·Š¾Š²Š°ŃŃ ŠŗŠ¾ŃŠ¾Ńкие Š½Š°Š·Š²Š°Š½ŠøŃ ŃŠ²ŠµŃов,ŠŠøŠŗŠ¾ŃŠøŃŃŠ¾Š²ŃŠ²Š°ŃŠø ŃŠŗŠ¾ŃŠ¾ŃŠµŠ½Ń назви ŠŗŠ¾Š»ŃоŃŃŠ²
+#compress_numbers,Compress numbers,Дмали ŃŠøŃŠ»Š°ŃŠ°,Nummern komprimieren,ДжаŃŃ ŃŠøŃŃŃ,Š”ŃŠøŃнŃŃŠø ŃŠøŃŃŠø
+#minimize_spacing,Minimize spacing,ŠŠ°Š¼Š°Š»Šø ŠæŠ°ŃŠ·ŠøŃе,Leerplatz minimieren,ŠŠøŠ½ŠøŠ¼ŠøŠ·ŠøŃоваŃŃ ŃŠ°Š·Š¼ŠµŃŃ,ŠŃŠ½ŃŠ¼ŃŠ·ŃŠ²Š°ŃŠø Š²ŃŠ“ŃŃŠ°Š½Ń
+#remove_spacing_after_flags,Remove spacing after flags,ŠŃемаŃ
ни ŠæŠ°ŃŠ·ŠøŃе ŃŠ»ŠµŠ“ ŃŠ»Š°Š³Š¾Š²ŠµŃе,LeerplƤtze nach Eigenschaften (flag) entfernen,УГалиŃŃ ŃŠ°ŃŃŃŠ¾Ńние ŠæŠ¾ŃŠ»Šµ ŃŠ»Š°Š³Š¾Š²,ŠŠøŠ“Š°Š»ŠøŃŠø Š²ŃŠ“ŃŃŠ°Š½Ń ŠæŃŃŠ»Ń ŠæŃŠ°ŠæŠ¾ŃŃŃŠ²
+#remove_consecutive_commands,Remove consecutive commands,ŠŃемаŃ
ни ŠæŠ¾ŃŠ»ŠµŠ“Š¾Š²Š°ŃŠµŠ»Š½ŠøŃе команГи,Aufeinanderfolgende Befehle entfernen,УГалиŃŃ ŠæŠ¾ŃŠ»ŠµŠ“Š¾Š²Š°ŃŠµŠ»ŃŠ½ŃŠµ команГŃ,ŠŠøŠ“Š°Š»ŠøŃŠø ŠæŠ¾ŃŠ»ŃŠ“Š¾Š²Š½Ń ŠŗŠ¾Š¼Š°Š½Š“Šø
+#remove_unnecessary_parameters,Remove unnecessary parameters,ŠŃемаŃ
ни Š½ŠµŠ½ŃжниŃе ŠæŠ°ŃŠ°Š¼ŠµŃŃŠø,Unnƶtige Parameter entfernen,,
+#relative,Relative,ŠŃŠ½Š¾ŃŠøŃелно,Relativ,ŠŃŠ½Š¾ŃŠøŃŠµŠ»ŃŠ½Š°Ń,ŠŃŠ“Š½Š¾ŃŠ½Š°
+#absolute,Absolute,ŠŠ±ŃолŃŃŠ½Š¾,Absolut,ŠŠ±ŃолŃŃŠ½Š°Ń,ŠŠ±ŃолŃŃŠ½Š°
+#remove_association,Remove the association,ŠŃемаŃ
ни Š°ŃŠ¾ŃŠøŠ°ŃŠøŃŃŠ°,Dateizuordnung entfernen,,
+#apply_matrix,Apply the matrix,ŠŃиложи маŃŃŠøŃŠ°ŃŠ°,Die Matrix anwenden,,
+#search_color,Search color,Š¢ŃŃŃŠµŠ½Šµ на ŃŠ²ŃŃ,Farbe suchen,,
+#syntax_error,Syntax error,Š”ŠøŠ½ŃŠ°ŠŗŃŠøŃŠ½Š° Š³ŃŠµŃка,,,
diff --git a/translations/translation_sheet.csv.import b/translations/translation_sheet.csv.import
new file mode 100644
index 000000000..d27ea3350
--- /dev/null
+++ b/translations/translation_sheet.csv.import
@@ -0,0 +1,17 @@
+[remap]
+
+importer="csv_translation"
+type="Translation"
+uid="uid://bnuj54sahpohi"
+
+[deps]
+
+files=["res://translations/translation_sheet.en.translation", "res://translations/translation_sheet.bg.translation", "res://translations/translation_sheet.de.translation", "res://translations/translation_sheet.ru_RU.translation", "res://translations/translation_sheet.uk_UA.translation"]
+
+source_file="res://translations/translation_sheet.csv"
+dest_files=["res://translations/translation_sheet.en.translation", "res://translations/translation_sheet.bg.translation", "res://translations/translation_sheet.de.translation", "res://translations/translation_sheet.ru_RU.translation", "res://translations/translation_sheet.uk_UA.translation"]
+
+[params]
+
+compress=true
+delimiter=0
diff --git a/visual/Checkerboard.svg b/visual/Checkerboard.svg
deleted file mode 100644
index 1743afd61..000000000
--- a/visual/Checkerboard.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/visual/CodeFont.ttf b/visual/CodeFont.ttf
deleted file mode 100644
index 2793a9d35..000000000
Binary files a/visual/CodeFont.ttf and /dev/null differ
diff --git a/visual/Font.ttf b/visual/Font.ttf
deleted file mode 100644
index a31ca3f99..000000000
Binary files a/visual/Font.ttf and /dev/null differ
diff --git a/visual/fonts/Font.ttf b/visual/fonts/Font.ttf
new file mode 100644
index 000000000..b460754c5
Binary files /dev/null and b/visual/fonts/Font.ttf differ
diff --git a/visual/Font.ttf.import b/visual/fonts/Font.ttf.import
similarity index 64%
rename from visual/Font.ttf.import
rename to visual/fonts/Font.ttf.import
index 99f68a261..bde503cbe 100644
--- a/visual/Font.ttf.import
+++ b/visual/fonts/Font.ttf.import
@@ -2,13 +2,13 @@
importer="font_data_dynamic"
type="FontFile"
-uid="uid://bd8rlfqek8f8u"
-path="res://.godot/imported/Font.ttf-84187cb274cb0d16566f6262f25aa9cf.fontdata"
+uid="uid://clpf84p1lfwlp"
+path="res://.godot/imported/Font.ttf-ab23156a8f74313018e59a9482cc956b.fontdata"
[deps]
-source_file="res://visual/Font.ttf"
-dest_files=["res://.godot/imported/Font.ttf-84187cb274cb0d16566f6262f25aa9cf.fontdata"]
+source_file="res://visual/fonts/Font.ttf"
+dest_files=["res://.godot/imported/Font.ttf-ab23156a8f74313018e59a9482cc956b.fontdata"]
[params]
diff --git a/visual/fonts/FontBold.ttf b/visual/fonts/FontBold.ttf
new file mode 100644
index 000000000..a74c69ab1
Binary files /dev/null and b/visual/fonts/FontBold.ttf differ
diff --git a/visual/CodeFont.ttf.import b/visual/fonts/FontBold.ttf.import
similarity index 63%
rename from visual/CodeFont.ttf.import
rename to visual/fonts/FontBold.ttf.import
index 5464911e3..939a424fb 100644
--- a/visual/CodeFont.ttf.import
+++ b/visual/fonts/FontBold.ttf.import
@@ -2,13 +2,13 @@
importer="font_data_dynamic"
type="FontFile"
-uid="uid://c7ury252fql35"
-path="res://.godot/imported/CodeFont.ttf-0a98f2d3c4640aeb493abf7ae96c6704.fontdata"
+uid="uid://dc0w4sx0h0fui"
+path="res://.godot/imported/FontBold.ttf-1e1f4e9653596be23836f0f07c44b39f.fontdata"
[deps]
-source_file="res://visual/CodeFont.ttf"
-dest_files=["res://.godot/imported/CodeFont.ttf-0a98f2d3c4640aeb493abf7ae96c6704.fontdata"]
+source_file="res://visual/fonts/FontBold.ttf"
+dest_files=["res://.godot/imported/FontBold.ttf-1e1f4e9653596be23836f0f07c44b39f.fontdata"]
[params]
diff --git a/visual/fonts/FontMono.ttf b/visual/fonts/FontMono.ttf
new file mode 100644
index 000000000..ad93dc54c
Binary files /dev/null and b/visual/fonts/FontMono.ttf differ
diff --git a/visual/fonts/FontMono.ttf.import b/visual/fonts/FontMono.ttf.import
new file mode 100644
index 000000000..a98fb54d3
--- /dev/null
+++ b/visual/fonts/FontMono.ttf.import
@@ -0,0 +1,41 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://dtb4wkus51hxs"
+path="res://.godot/imported/FontMono.ttf-a376ecc2a480b8f7f1be2af334133b0b.fontdata"
+
+[deps]
+
+source_file="res://visual/fonts/FontMono.ttf"
+dest_files=["res://.godot/imported/FontMono.ttf-a376ecc2a480b8f7f1be2af334133b0b.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[Resource("res://visual/fonts/Font.ttf")]
+Compress=null
+compress=true
+preload=[{
+"chars": [],
+"glyphs": [],
+"name": "New Configuration",
+"size": Vector2i(16, 0),
+"variation_embolden": 0.0
+}]
+language_support={}
+script_support={
+"Latn": true
+}
+opentype_features={}
diff --git a/visual/icon.ico b/visual/icon.ico
new file mode 100644
index 000000000..0b6df78c4
Binary files /dev/null and b/visual/icon.ico differ
diff --git a/visual/icon.png b/visual/icon.png
new file mode 100644
index 000000000..21cc845b0
Binary files /dev/null and b/visual/icon.png differ
diff --git a/icon.png.import b/visual/icon.png.import
similarity index 73%
rename from icon.png.import
rename to visual/icon.png.import
index 41b79bcc8..98442ec6a 100644
--- a/icon.png.import
+++ b/visual/icon.png.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://v7hej5yexkvl"
-path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"
+path="res://.godot/imported/icon.png-e9d1429201c190d11a3351ce85f1bc64.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://icon.png"
-dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"]
+source_file="res://visual/icon.png"
+dest_files=["res://.godot/imported/icon.png-e9d1429201c190d11a3351ce85f1bc64.ctex"]
[params]
diff --git a/visual/icon.svg b/visual/icon.svg
new file mode 100644
index 000000000..30a625f8d
--- /dev/null
+++ b/visual/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Up.svg.import b/visual/icon.svg.import
similarity index 73%
rename from visual/icons/Up.svg.import
rename to visual/icon.svg.import
index db1ce21a4..b25e82e40 100644
--- a/visual/icons/Up.svg.import
+++ b/visual/icon.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://wburo8uyoqy4"
-path="res://.godot/imported/Up.svg-353810afc8118db47537c6ce7a2d1ace.ctex"
+uid="uid://barsurula6j8n"
+path="res://.godot/imported/icon.svg-4135024f1e47fffc8bac3039c30445db.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://visual/icons/Up.svg"
-dest_files=["res://.godot/imported/Up.svg-353810afc8118db47537c6ce7a2d1ace.ctex"]
+source_file="res://visual/icon.svg"
+dest_files=["res://.godot/imported/icon.svg-4135024f1e47fffc8bac3039c30445db.ctex"]
[params]
diff --git a/visual/icons/ApplyMatrix.svg b/visual/icons/ApplyMatrix.svg
new file mode 100644
index 000000000..b21996b2f
--- /dev/null
+++ b/visual/icons/ApplyMatrix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/ApplyMatrix.svg.import b/visual/icons/ApplyMatrix.svg.import
new file mode 100644
index 000000000..b83dd8905
--- /dev/null
+++ b/visual/icons/ApplyMatrix.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cqg7ga6y3m0v1"
+path="res://.godot/imported/ApplyMatrix.svg-85441c3c62f19555e11c22758f980cc0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/ApplyMatrix.svg"
+dest_files=["res://.godot/imported/ApplyMatrix.svg-85441c3c62f19555e11c22758f980cc0.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/visual/icons/Arrow.svg b/visual/icons/Arrow.svg
new file mode 100644
index 000000000..8aa72c0a2
--- /dev/null
+++ b/visual/icons/Arrow.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Down.svg.import b/visual/icons/Arrow.svg.import
similarity index 76%
rename from visual/icons/Down.svg.import
rename to visual/icons/Arrow.svg.import
index e3b103579..50c56980f 100644
--- a/visual/icons/Down.svg.import
+++ b/visual/icons/Arrow.svg.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://coda6chhcatal"
-path="res://.godot/imported/Down.svg-6bf8e0aa755885fa0cc0b3707ef59228.ctex"
+path="res://.godot/imported/Arrow.svg-06042df8f20de397330083634dd52037.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://visual/icons/Down.svg"
-dest_files=["res://.godot/imported/Down.svg-6bf8e0aa755885fa0cc0b3707ef59228.ctex"]
+source_file="res://visual/icons/Arrow.svg"
+dest_files=["res://.godot/imported/Arrow.svg-06042df8f20de397330083634dd52037.ctex"]
[params]
diff --git a/visual/icons/Circle.svg b/visual/icons/Circle.svg
deleted file mode 100644
index f2c867682..000000000
--- a/visual/icons/Circle.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/visual/icons/Compress.svg b/visual/icons/Compress.svg
new file mode 100644
index 000000000..e3892a6a0
--- /dev/null
+++ b/visual/icons/Compress.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Compress.svg.import b/visual/icons/Compress.svg.import
new file mode 100644
index 000000000..efefae867
--- /dev/null
+++ b/visual/icons/Compress.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5kgvxffw35gi"
+path="res://.godot/imported/Compress.svg-5a36a4a01a7f614fe441ad5c7140a46a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Compress.svg"
+dest_files=["res://.godot/imported/Compress.svg-5a36a4a01a7f614fe441ad5c7140a46a.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/visual/icons/Copy.svg b/visual/icons/Copy.svg
new file mode 100644
index 000000000..f4947a79b
--- /dev/null
+++ b/visual/icons/Copy.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Cross.svg.import b/visual/icons/Copy.svg.import
similarity index 73%
rename from visual/icons/Cross.svg.import
rename to visual/icons/Copy.svg.import
index 9575aa0c0..1731ec0ff 100644
--- a/visual/icons/Cross.svg.import
+++ b/visual/icons/Copy.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bywp6c8s6meq1"
-path="res://.godot/imported/Cross.svg-1addab1622bb50a6f721b72bae14687f.ctex"
+uid="uid://ccvjkdd0s7rb4"
+path="res://.godot/imported/Copy.svg-7e33a9d04f1760f77f046a9130800e38.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://visual/icons/Cross.svg"
-dest_files=["res://.godot/imported/Cross.svg-1addab1622bb50a6f721b72bae14687f.ctex"]
+source_file="res://visual/icons/Copy.svg"
+dest_files=["res://.godot/imported/Copy.svg-7e33a9d04f1760f77f046a9130800e38.ctex"]
[params]
diff --git a/visual/icons/Coupled.svg b/visual/icons/Coupled.svg
new file mode 100644
index 000000000..c9dd51979
--- /dev/null
+++ b/visual/icons/Coupled.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Ellipse.svg.import b/visual/icons/Coupled.svg.import
similarity index 72%
rename from visual/icons/Ellipse.svg.import
rename to visual/icons/Coupled.svg.import
index 11285401f..726539b42 100644
--- a/visual/icons/Ellipse.svg.import
+++ b/visual/icons/Coupled.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://c8t8eiojuwjje"
-path="res://.godot/imported/Ellipse.svg-999878c9b23e7ee08cb14dcce899c11f.ctex"
+uid="uid://bv4lcvienlyfa"
+path="res://.godot/imported/Coupled.svg-3d6033e33c0d683c66c8a06e6e4a2fe2.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://visual/icons/Ellipse.svg"
-dest_files=["res://.godot/imported/Ellipse.svg-999878c9b23e7ee08cb14dcce899c11f.ctex"]
+source_file="res://visual/icons/Coupled.svg"
+dest_files=["res://.godot/imported/Coupled.svg-3d6033e33c0d683c66c8a06e6e4a2fe2.ctex"]
[params]
diff --git a/visual/icons/Cross.svg b/visual/icons/Cross.svg
deleted file mode 100644
index a36b2cb04..000000000
--- a/visual/icons/Cross.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/visual/icons/Decoupled.svg b/visual/icons/Decoupled.svg
new file mode 100644
index 000000000..bd4443756
--- /dev/null
+++ b/visual/icons/Decoupled.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Decoupled.svg.import b/visual/icons/Decoupled.svg.import
new file mode 100644
index 000000000..3d44eeba5
--- /dev/null
+++ b/visual/icons/Decoupled.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://3bnqye3n8fjx"
+path="res://.godot/imported/Decoupled.svg-ebb469cb41419e74f42f3ef77e3f9498.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Decoupled.svg"
+dest_files=["res://.godot/imported/Decoupled.svg-ebb469cb41419e74f42f3ef77e3f9498.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/visual/icons/Delete.svg b/visual/icons/Delete.svg
new file mode 100644
index 000000000..fbe253e16
--- /dev/null
+++ b/visual/icons/Delete.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Circle.svg.import b/visual/icons/Delete.svg.import
similarity index 72%
rename from visual/icons/Circle.svg.import
rename to visual/icons/Delete.svg.import
index 1d16881da..a913fe025 100644
--- a/visual/icons/Circle.svg.import
+++ b/visual/icons/Delete.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://dmnaqiwq4de1j"
-path="res://.godot/imported/Circle.svg-67939871ffdebf6447da99b5129acefe.ctex"
+uid="uid://cj5x2ti8150ja"
+path="res://.godot/imported/Delete.svg-6b52ae4e460482dae4928e13c83b94c0.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://visual/icons/Circle.svg"
-dest_files=["res://.godot/imported/Circle.svg-67939871ffdebf6447da99b5129acefe.ctex"]
+source_file="res://visual/icons/Delete.svg"
+dest_files=["res://.godot/imported/Delete.svg-6b52ae4e460482dae4928e13c83b94c0.ctex"]
[params]
diff --git a/visual/icons/Docs.svg b/visual/icons/Docs.svg
new file mode 100644
index 000000000..665ffd8b1
--- /dev/null
+++ b/visual/icons/Docs.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Docs.svg.import b/visual/icons/Docs.svg.import
new file mode 100644
index 000000000..a18f52d2e
--- /dev/null
+++ b/visual/icons/Docs.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dn20bhrip8a32"
+path="res://.godot/imported/Docs.svg-0b0cbcf520be99bac9e1609ef6cc2d8d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Docs.svg"
+dest_files=["res://.godot/imported/Docs.svg-0b0cbcf520be99bac9e1609ef6cc2d8d.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/visual/icons/Down.svg b/visual/icons/Down.svg
deleted file mode 100644
index 583bf8d9c..000000000
--- a/visual/icons/Down.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/visual/icons/Duplicate.svg b/visual/icons/Duplicate.svg
new file mode 100644
index 000000000..e8e932353
--- /dev/null
+++ b/visual/icons/Duplicate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Duplicate.svg.import b/visual/icons/Duplicate.svg.import
new file mode 100644
index 000000000..ae2dc9925
--- /dev/null
+++ b/visual/icons/Duplicate.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c47mt41k028j2"
+path="res://.godot/imported/Duplicate.svg-a96e56abfa87da0d41172358b51ddd87.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Duplicate.svg"
+dest_files=["res://.godot/imported/Duplicate.svg-a96e56abfa87da0d41172358b51ddd87.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/visual/icons/Edit.svg b/visual/icons/Edit.svg
new file mode 100644
index 000000000..df6250d8c
--- /dev/null
+++ b/visual/icons/Edit.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Edit.svg.import b/visual/icons/Edit.svg.import
new file mode 100644
index 000000000..1b2757e46
--- /dev/null
+++ b/visual/icons/Edit.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dr2erka82g6j4"
+path="res://.godot/imported/Edit.svg-e5dee2c17c2a6c10301ffeb8557d98f5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Edit.svg"
+dest_files=["res://.godot/imported/Edit.svg-e5dee2c17c2a6c10301ffeb8557d98f5.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/visual/icons/Ellipse.svg b/visual/icons/Ellipse.svg
deleted file mode 100644
index dd78df586..000000000
--- a/visual/icons/Ellipse.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/visual/icons/Export.svg b/visual/icons/Export.svg
new file mode 100644
index 000000000..ee331a522
--- /dev/null
+++ b/visual/icons/Export.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Export.svg.import b/visual/icons/Export.svg.import
new file mode 100644
index 000000000..31bcf8649
--- /dev/null
+++ b/visual/icons/Export.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d0uvwj0t44n6v"
+path="res://.godot/imported/Export.svg-eacbdbc551152fc9aa15a8742664dc96.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Export.svg"
+dest_files=["res://.godot/imported/Export.svg-eacbdbc551152fc9aa15a8742664dc96.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/visual/icons/Gear.svg b/visual/icons/Gear.svg
new file mode 100644
index 000000000..3cb6af164
--- /dev/null
+++ b/visual/icons/Gear.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Gear.svg.import b/visual/icons/Gear.svg.import
new file mode 100644
index 000000000..4a30dbc7a
--- /dev/null
+++ b/visual/icons/Gear.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ckkkgof1hcbld"
+path="res://.godot/imported/Gear.svg-226d4138ca06ad01c6b34d0babb9a611.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Gear.svg"
+dest_files=["res://.godot/imported/Gear.svg-226d4138ca06ad01c6b34d0babb9a611.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/visual/icons/GearOutlined.svg b/visual/icons/GearOutlined.svg
new file mode 100644
index 000000000..eb75329b7
--- /dev/null
+++ b/visual/icons/GearOutlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/GearOutlined.svg.import b/visual/icons/GearOutlined.svg.import
new file mode 100644
index 000000000..db07644fd
--- /dev/null
+++ b/visual/icons/GearOutlined.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bhxr0nv6bs5j2"
+path="res://.godot/imported/GearOutlined.svg-fb7b428da24b6bf77179a772ac33e058.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/GearOutlined.svg"
+dest_files=["res://.godot/imported/GearOutlined.svg-fb7b428da24b6bf77179a772ac33e058.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/visual/icons/Heart.svg b/visual/icons/Heart.svg
new file mode 100644
index 000000000..c4a056b2b
--- /dev/null
+++ b/visual/icons/Heart.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Heart.svg.import b/visual/icons/Heart.svg.import
new file mode 100644
index 000000000..07842c432
--- /dev/null
+++ b/visual/icons/Heart.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bvm6h1qda67ys"
+path="res://.godot/imported/Heart.svg-d1f3e41c521e9a05fafb36726218b620.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Heart.svg"
+dest_files=["res://.godot/imported/Heart.svg-d1f3e41c521e9a05fafb36726218b620.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/visual/icons/Import.svg b/visual/icons/Import.svg
new file mode 100644
index 000000000..a402d8b2a
--- /dev/null
+++ b/visual/icons/Import.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Import.svg.import b/visual/icons/Import.svg.import
new file mode 100644
index 000000000..295c14824
--- /dev/null
+++ b/visual/icons/Import.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://6ymbl3jqersp"
+path="res://.godot/imported/Import.svg-d30fdeea68f95dc865c354812697f82b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Import.svg"
+dest_files=["res://.godot/imported/Import.svg-d30fdeea68f95dc865c354812697f82b.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/visual/icons/InsertAfter.svg b/visual/icons/InsertAfter.svg
new file mode 100644
index 000000000..beeb66d3b
--- /dev/null
+++ b/visual/icons/InsertAfter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/InsertAfter.svg.import b/visual/icons/InsertAfter.svg.import
new file mode 100644
index 000000000..37d5c10e7
--- /dev/null
+++ b/visual/icons/InsertAfter.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5iyi2abx0ja1"
+path="res://.godot/imported/InsertAfter.svg-c4347bcebe6a850de014fa5769f69e11.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/InsertAfter.svg"
+dest_files=["res://.godot/imported/InsertAfter.svg-c4347bcebe6a850de014fa5769f69e11.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/visual/icons/InsertBefore.svg b/visual/icons/InsertBefore.svg
new file mode 100644
index 000000000..2431abe17
--- /dev/null
+++ b/visual/icons/InsertBefore.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/InsertBefore.svg.import b/visual/icons/InsertBefore.svg.import
new file mode 100644
index 000000000..8397fbdaf
--- /dev/null
+++ b/visual/icons/InsertBefore.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dav1phw6au0c1"
+path="res://.godot/imported/InsertBefore.svg-1891196005597a7a2989220268121f4e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/InsertBefore.svg"
+dest_files=["res://.godot/imported/InsertBefore.svg-1891196005597a7a2989220268121f4e.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/visual/icons/Languages.svg b/visual/icons/Languages.svg
new file mode 100644
index 000000000..ca4439f6f
--- /dev/null
+++ b/visual/icons/Languages.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Languages.svg.import b/visual/icons/Languages.svg.import
new file mode 100644
index 000000000..8f2fe7dae
--- /dev/null
+++ b/visual/icons/Languages.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c528knojuxbw6"
+path="res://.godot/imported/Languages.svg-3edf7294fd0402c1da641f8f9a267120.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Languages.svg"
+dest_files=["res://.godot/imported/Languages.svg-3edf7294fd0402c1da641f8f9a267120.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/visual/icons/Link.svg b/visual/icons/Link.svg
new file mode 100644
index 000000000..889ccc1d0
--- /dev/null
+++ b/visual/icons/Link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Link.svg.import b/visual/icons/Link.svg.import
new file mode 100644
index 000000000..1d365235a
--- /dev/null
+++ b/visual/icons/Link.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cgxpm1e3v0i3v"
+path="res://.godot/imported/Link.svg-b275e609ae3d23dd9c12467a9eaccb3e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Link.svg"
+dest_files=["res://.godot/imported/Link.svg-b275e609ae3d23dd9c12467a9eaccb3e.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/visual/icons/Matrix.svg b/visual/icons/Matrix.svg
new file mode 100644
index 000000000..bddaa9c7b
--- /dev/null
+++ b/visual/icons/Matrix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Matrix.svg.import b/visual/icons/Matrix.svg.import
new file mode 100644
index 000000000..bef54db3a
--- /dev/null
+++ b/visual/icons/Matrix.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://crtn52j7mlh11"
+path="res://.godot/imported/Matrix.svg-50ccb4c66db121034fb4bd8ffaf9f823.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Matrix.svg"
+dest_files=["res://.godot/imported/Matrix.svg-50ccb4c66db121034fb4bd8ffaf9f823.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/visual/icons/Minus.svg b/visual/icons/Minus.svg
new file mode 100644
index 000000000..405dd9556
--- /dev/null
+++ b/visual/icons/Minus.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Minus.svg.import b/visual/icons/Minus.svg.import
new file mode 100644
index 000000000..cd1a58b54
--- /dev/null
+++ b/visual/icons/Minus.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c2h5snkvemm4p"
+path="res://.godot/imported/Minus.svg-6d4ddb6244a94e6ab29f4f7a8b1dc29e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Minus.svg"
+dest_files=["res://.godot/imported/Minus.svg-6d4ddb6244a94e6ab29f4f7a8b1dc29e.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/visual/icons/More.svg b/visual/icons/More.svg
new file mode 100644
index 000000000..198b72756
--- /dev/null
+++ b/visual/icons/More.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/More.svg.import b/visual/icons/More.svg.import
new file mode 100644
index 000000000..ad89155bd
--- /dev/null
+++ b/visual/icons/More.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ccbta5q43jobk"
+path="res://.godot/imported/More.svg-68029a77f6adbeb369f91e1bc201c533.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/More.svg"
+dest_files=["res://.godot/imported/More.svg-68029a77f6adbeb369f91e1bc201c533.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/visual/icons/MoveDown.svg b/visual/icons/MoveDown.svg
new file mode 100644
index 000000000..a49608618
--- /dev/null
+++ b/visual/icons/MoveDown.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/MoveDown.svg.import b/visual/icons/MoveDown.svg.import
new file mode 100644
index 000000000..8e463ba79
--- /dev/null
+++ b/visual/icons/MoveDown.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ciirlvjd2efd3"
+path="res://.godot/imported/MoveDown.svg-8705323c42c89169e30f63736c2290ec.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/MoveDown.svg"
+dest_files=["res://.godot/imported/MoveDown.svg-8705323c42c89169e30f63736c2290ec.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/visual/icons/MoveUp.svg b/visual/icons/MoveUp.svg
new file mode 100644
index 000000000..e140099c8
--- /dev/null
+++ b/visual/icons/MoveUp.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/MoveUp.svg.import b/visual/icons/MoveUp.svg.import
new file mode 100644
index 000000000..9c1998d10
--- /dev/null
+++ b/visual/icons/MoveUp.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dgfyegqnsoqgd"
+path="res://.godot/imported/MoveUp.svg-476a742e19b47aac065618bd710e5938.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/MoveUp.svg"
+dest_files=["res://.godot/imported/MoveUp.svg-476a742e19b47aac065618bd710e5938.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/visual/icons/NoneColor.svg b/visual/icons/NoneColor.svg
new file mode 100644
index 000000000..30c292ab5
--- /dev/null
+++ b/visual/icons/NoneColor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/NoneColor.svg.import b/visual/icons/NoneColor.svg.import
new file mode 100644
index 000000000..de43ac355
--- /dev/null
+++ b/visual/icons/NoneColor.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d36qn2f7a0nok"
+path="res://.godot/imported/NoneColor.svg-035e27ac232b9837a5db250d09cebaa9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/NoneColor.svg"
+dest_files=["res://.godot/imported/NoneColor.svg-035e27ac232b9837a5db250d09cebaa9.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/visual/icons/Plus.svg b/visual/icons/Plus.svg
new file mode 100644
index 000000000..39f936817
--- /dev/null
+++ b/visual/icons/Plus.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Rect.svg.import b/visual/icons/Plus.svg.import
similarity index 73%
rename from visual/icons/Rect.svg.import
rename to visual/icons/Plus.svg.import
index 0e4d6cb4b..72ef8adf7 100644
--- a/visual/icons/Rect.svg.import
+++ b/visual/icons/Plus.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cso2l5nvm6gm"
-path="res://.godot/imported/Rect.svg-6904e70903749c997c6b2c3ea969dbec.ctex"
+uid="uid://eif2ioi0mw17"
+path="res://.godot/imported/Plus.svg-23cb888c0ab80094e07ec795584fa07c.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://visual/icons/Rect.svg"
-dest_files=["res://.godot/imported/Rect.svg-6904e70903749c997c6b2c3ea969dbec.ctex"]
+source_file="res://visual/icons/Plus.svg"
+dest_files=["res://.godot/imported/Plus.svg-23cb888c0ab80094e07ec795584fa07c.ctex"]
[params]
diff --git a/visual/icons/QuestionMark.svg b/visual/icons/QuestionMark.svg
new file mode 100644
index 000000000..5fe10d356
--- /dev/null
+++ b/visual/icons/QuestionMark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/QuestionMark.svg.import b/visual/icons/QuestionMark.svg.import
new file mode 100644
index 000000000..d5af8bf9b
--- /dev/null
+++ b/visual/icons/QuestionMark.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bi38k4pq83omf"
+path="res://.godot/imported/QuestionMark.svg-706a36696e12b13ee00f5b609867eda1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/QuestionMark.svg"
+dest_files=["res://.godot/imported/QuestionMark.svg-706a36696e12b13ee00f5b609867eda1.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/visual/icons/Rect.svg b/visual/icons/Rect.svg
deleted file mode 100644
index 36dc87254..000000000
--- a/visual/icons/Rect.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/visual/icons/Reload.svg b/visual/icons/Reload.svg
new file mode 100644
index 000000000..902ebd9ce
--- /dev/null
+++ b/visual/icons/Reload.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Reload.svg.import b/visual/icons/Reload.svg.import
new file mode 100644
index 000000000..18ea7fa85
--- /dev/null
+++ b/visual/icons/Reload.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cvh3kwbucf2n1"
+path="res://.godot/imported/Reload.svg-1e3724a1c699a578c5d203ced4804f13.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Reload.svg"
+dest_files=["res://.godot/imported/Reload.svg-1e3724a1c699a578c5d203ced4804f13.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/visual/icons/Rotate.svg b/visual/icons/Rotate.svg
new file mode 100644
index 000000000..ad18ae930
--- /dev/null
+++ b/visual/icons/Rotate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Rotate.svg.import b/visual/icons/Rotate.svg.import
new file mode 100644
index 000000000..d3d64bf16
--- /dev/null
+++ b/visual/icons/Rotate.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://mop8xvnp8qcv"
+path="res://.godot/imported/Rotate.svg-c7caf5f8cfbb912608d36310b5f83a91.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Rotate.svg"
+dest_files=["res://.godot/imported/Rotate.svg-c7caf5f8cfbb912608d36310b5f83a91.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/visual/icons/Scale.svg b/visual/icons/Scale.svg
new file mode 100644
index 000000000..c9fe715c2
--- /dev/null
+++ b/visual/icons/Scale.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Scale.svg.import b/visual/icons/Scale.svg.import
new file mode 100644
index 000000000..f1704c63c
--- /dev/null
+++ b/visual/icons/Scale.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b2cpjg74rpkjs"
+path="res://.godot/imported/Scale.svg-359378c9a1c7f6bef869ecb7b7cd43ad.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Scale.svg"
+dest_files=["res://.godot/imported/Scale.svg-359378c9a1c7f6bef869ecb7b7cd43ad.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/visual/icons/SideSliderArrow.svg b/visual/icons/SideSliderArrow.svg
new file mode 100644
index 000000000..7b5670e01
--- /dev/null
+++ b/visual/icons/SideSliderArrow.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/SideSliderArrow.svg.import b/visual/icons/SideSliderArrow.svg.import
new file mode 100644
index 000000000..63f9793fa
--- /dev/null
+++ b/visual/icons/SideSliderArrow.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cmu6r4ipti60x"
+path="res://.godot/imported/SideSliderArrow.svg-71eb756fca2bc3834582408862523b24.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/SideSliderArrow.svg"
+dest_files=["res://.godot/imported/SideSliderArrow.svg-71eb756fca2bc3834582408862523b24.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/visual/icons/SkewX.svg b/visual/icons/SkewX.svg
new file mode 100644
index 000000000..b3a1cc1b3
--- /dev/null
+++ b/visual/icons/SkewX.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/SkewX.svg.import b/visual/icons/SkewX.svg.import
new file mode 100644
index 000000000..43fb80e37
--- /dev/null
+++ b/visual/icons/SkewX.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://mkfxc0srhfek"
+path="res://.godot/imported/SkewX.svg-56890bb7794b621acab9fa8f99847072.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/SkewX.svg"
+dest_files=["res://.godot/imported/SkewX.svg-56890bb7794b621acab9fa8f99847072.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/visual/icons/SkewY.svg b/visual/icons/SkewY.svg
new file mode 100644
index 000000000..21b70b13a
--- /dev/null
+++ b/visual/icons/SkewY.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/SkewY.svg.import b/visual/icons/SkewY.svg.import
new file mode 100644
index 000000000..4ee09b86e
--- /dev/null
+++ b/visual/icons/SkewY.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c7mdaue5rmbt3"
+path="res://.godot/imported/SkewY.svg-1daad0e69d015563fea2c70d36f497b7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/SkewY.svg"
+dest_files=["res://.godot/imported/SkewY.svg-1daad0e69d015563fea2c70d36f497b7.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/visual/icons/SliderArrow.svg b/visual/icons/SliderArrow.svg
new file mode 100644
index 000000000..4f8c81933
--- /dev/null
+++ b/visual/icons/SliderArrow.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/SliderArrow.svg.import b/visual/icons/SliderArrow.svg.import
new file mode 100644
index 000000000..d45b6b3a6
--- /dev/null
+++ b/visual/icons/SliderArrow.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cnq1u5hrcqrho"
+path="res://.godot/imported/SliderArrow.svg-6b7d47b794eb3da71fd45e482d52179a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/SliderArrow.svg"
+dest_files=["res://.godot/imported/SliderArrow.svg-6b7d47b794eb3da71fd45e482d52179a.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/visual/icons/SmallMore.svg b/visual/icons/SmallMore.svg
new file mode 100644
index 000000000..115d5d495
--- /dev/null
+++ b/visual/icons/SmallMore.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/SmallMore.svg.import b/visual/icons/SmallMore.svg.import
new file mode 100644
index 000000000..328cace1d
--- /dev/null
+++ b/visual/icons/SmallMore.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cmepkbqde0jh0"
+path="res://.godot/imported/SmallMore.svg-ae74108d3677c7a59c41391c15546438.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/SmallMore.svg"
+dest_files=["res://.godot/imported/SmallMore.svg-ae74108d3677c7a59c41391c15546438.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/visual/icons/SmallQuestionMark.svg b/visual/icons/SmallQuestionMark.svg
new file mode 100644
index 000000000..c788f1964
--- /dev/null
+++ b/visual/icons/SmallQuestionMark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/SmallQuestionMark.svg.import b/visual/icons/SmallQuestionMark.svg.import
new file mode 100644
index 000000000..d08102944
--- /dev/null
+++ b/visual/icons/SmallQuestionMark.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://crx4kcj4o01bs"
+path="res://.godot/imported/SmallQuestionMark.svg-896c2402fe31c497e9a9a3ae5f5cca9a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/SmallQuestionMark.svg"
+dest_files=["res://.godot/imported/SmallQuestionMark.svg-896c2402fe31c497e9a9a3ae5f5cca9a.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/visual/icons/Snap.svg b/visual/icons/Snap.svg
new file mode 100644
index 000000000..49a7f280d
--- /dev/null
+++ b/visual/icons/Snap.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Snap.svg.import b/visual/icons/Snap.svg.import
new file mode 100644
index 000000000..dadf2acf3
--- /dev/null
+++ b/visual/icons/Snap.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://buire51l0mifg"
+path="res://.godot/imported/Snap.svg-aaf873ca94baf0be5c4181b38842ca88.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Snap.svg"
+dest_files=["res://.godot/imported/Snap.svg-aaf873ca94baf0be5c4181b38842ca88.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/visual/icons/Translate.svg b/visual/icons/Translate.svg
new file mode 100644
index 000000000..c238b6a26
--- /dev/null
+++ b/visual/icons/Translate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/Translate.svg.import b/visual/icons/Translate.svg.import
new file mode 100644
index 000000000..3581d97fe
--- /dev/null
+++ b/visual/icons/Translate.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b7r171i82g3xl"
+path="res://.godot/imported/Translate.svg-cacb854b6c1e693d938f9f1ee2332d07.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Translate.svg"
+dest_files=["res://.godot/imported/Translate.svg-cacb854b6c1e693d938f9f1ee2332d07.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/visual/icons/Up.svg b/visual/icons/Up.svg
deleted file mode 100644
index 62cd0ece2..000000000
--- a/visual/icons/Up.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/visual/icons/Visuals.svg b/visual/icons/Visuals.svg
new file mode 100644
index 000000000..bde388591
--- /dev/null
+++ b/visual/icons/Visuals.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/Visuals.svg.import b/visual/icons/Visuals.svg.import
new file mode 100644
index 000000000..9171d343e
--- /dev/null
+++ b/visual/icons/Visuals.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://kkxyv1gyrjgj"
+path="res://.godot/imported/Visuals.svg-60921e039c6a3b2059ba1e1c9e297fcf.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/Visuals.svg"
+dest_files=["res://.godot/imported/Visuals.svg-60921e039c6a3b2059ba1e1c9e297fcf.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/visual/icons/backgrounds/Checkerboard.svg b/visual/icons/backgrounds/Checkerboard.svg
new file mode 100644
index 000000000..e5390806c
--- /dev/null
+++ b/visual/icons/backgrounds/Checkerboard.svg
@@ -0,0 +1 @@
+
diff --git a/visual/Checkerboard.svg.import b/visual/icons/backgrounds/Checkerboard.svg.import
similarity index 73%
rename from visual/Checkerboard.svg.import
rename to visual/icons/backgrounds/Checkerboard.svg.import
index e9d752b6c..62654e764 100644
--- a/visual/Checkerboard.svg.import
+++ b/visual/icons/backgrounds/Checkerboard.svg.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://c68og6bsqt0lb"
-path="res://.godot/imported/Checkerboard.svg-191ced68db296dff65df7ce4a3ae428b.ctex"
+path="res://.godot/imported/Checkerboard.svg-1ca7da602c821f83425dbd4b34ca6a4f.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://visual/Checkerboard.svg"
-dest_files=["res://.godot/imported/Checkerboard.svg-191ced68db296dff65df7ce4a3ae428b.ctex"]
+source_file="res://visual/icons/backgrounds/Checkerboard.svg"
+dest_files=["res://.godot/imported/Checkerboard.svg-1ca7da602c821f83425dbd4b34ca6a4f.ctex"]
[params]
diff --git a/visual/icons/backgrounds/CheckerboardMini.svg b/visual/icons/backgrounds/CheckerboardMini.svg
new file mode 100644
index 000000000..0f1f3d13f
--- /dev/null
+++ b/visual/icons/backgrounds/CheckerboardMini.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/backgrounds/CheckerboardMini.svg.import b/visual/icons/backgrounds/CheckerboardMini.svg.import
new file mode 100644
index 000000000..92b1d6c11
--- /dev/null
+++ b/visual/icons/backgrounds/CheckerboardMini.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://stpallv5q0rb"
+path="res://.godot/imported/CheckerboardMini.svg-baf28e8b9517e069c5be3205f847ba8c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/backgrounds/CheckerboardMini.svg"
+dest_files=["res://.godot/imported/CheckerboardMini.svg-baf28e8b9517e069c5be3205f847ba8c.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/visual/icons/backgrounds/ColorButtonBG.svg b/visual/icons/backgrounds/ColorButtonBG.svg
new file mode 100644
index 000000000..2d1542417
--- /dev/null
+++ b/visual/icons/backgrounds/ColorButtonBG.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/backgrounds/ColorButtonBG.svg.import b/visual/icons/backgrounds/ColorButtonBG.svg.import
new file mode 100644
index 000000000..27035f6f3
--- /dev/null
+++ b/visual/icons/backgrounds/ColorButtonBG.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://y0l74x73w0co"
+path="res://.godot/imported/ColorButtonBG.svg-faa73f9c26fd9ec929af3bcf2f359ee1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/backgrounds/ColorButtonBG.svg"
+dest_files=["res://.godot/imported/ColorButtonBG.svg-faa73f9c26fd9ec929af3bcf2f359ee1.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/visual/icons/handles/HandleBig.svg b/visual/icons/handles/HandleBig.svg
new file mode 100644
index 000000000..3d7352e67
--- /dev/null
+++ b/visual/icons/handles/HandleBig.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/handles/HandleBig.svg.import b/visual/icons/handles/HandleBig.svg.import
new file mode 100644
index 000000000..1f2cea35d
--- /dev/null
+++ b/visual/icons/handles/HandleBig.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c05tk0wqo2fl1"
+path="res://.godot/imported/HandleBig.svg-5554f90b6d3c48504efaf769f7ce142b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/handles/HandleBig.svg"
+dest_files=["res://.godot/imported/HandleBig.svg-5554f90b6d3c48504efaf769f7ce142b.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/visual/icons/handles/HandleBigHovered.svg b/visual/icons/handles/HandleBigHovered.svg
new file mode 100644
index 000000000..49715a6d6
--- /dev/null
+++ b/visual/icons/handles/HandleBigHovered.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/handles/HandleBigHovered.svg.import b/visual/icons/handles/HandleBigHovered.svg.import
new file mode 100644
index 000000000..241282ffe
--- /dev/null
+++ b/visual/icons/handles/HandleBigHovered.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cnps3rthb043k"
+path="res://.godot/imported/HandleBigHovered.svg-6794f3458d6bbb6eda04eea2497dcdc4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/handles/HandleBigHovered.svg"
+dest_files=["res://.godot/imported/HandleBigHovered.svg-6794f3458d6bbb6eda04eea2497dcdc4.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/visual/icons/handles/HandleBigHoveredSelected.svg b/visual/icons/handles/HandleBigHoveredSelected.svg
new file mode 100644
index 000000000..0adfb1baf
--- /dev/null
+++ b/visual/icons/handles/HandleBigHoveredSelected.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/handles/HandleBigHoveredSelected.svg.import b/visual/icons/handles/HandleBigHoveredSelected.svg.import
new file mode 100644
index 000000000..97ffd0169
--- /dev/null
+++ b/visual/icons/handles/HandleBigHoveredSelected.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://54hm4c330xay"
+path="res://.godot/imported/HandleBigHoveredSelected.svg-ae14d420d3fb59526717ea6469c16359.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/handles/HandleBigHoveredSelected.svg"
+dest_files=["res://.godot/imported/HandleBigHoveredSelected.svg-ae14d420d3fb59526717ea6469c16359.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/visual/icons/handles/HandleBigSelected.svg b/visual/icons/handles/HandleBigSelected.svg
new file mode 100644
index 000000000..7b7d5818f
--- /dev/null
+++ b/visual/icons/handles/HandleBigSelected.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/handles/HandleBigSelected.svg.import b/visual/icons/handles/HandleBigSelected.svg.import
new file mode 100644
index 000000000..86868d6a7
--- /dev/null
+++ b/visual/icons/handles/HandleBigSelected.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bq71wvmb16i2r"
+path="res://.godot/imported/HandleBigSelected.svg-3d513c42fcaead1e73143a794b5b7ba9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/handles/HandleBigSelected.svg"
+dest_files=["res://.godot/imported/HandleBigSelected.svg-3d513c42fcaead1e73143a794b5b7ba9.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/visual/icons/handles/HandleSmall.svg b/visual/icons/handles/HandleSmall.svg
new file mode 100644
index 000000000..c2f2fefba
--- /dev/null
+++ b/visual/icons/handles/HandleSmall.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/handles/HandleSmall.svg.import b/visual/icons/handles/HandleSmall.svg.import
new file mode 100644
index 000000000..29fdc4496
--- /dev/null
+++ b/visual/icons/handles/HandleSmall.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://nb4ndwisnlen"
+path="res://.godot/imported/HandleSmall.svg-810fdec38a338826427b31eec34e909e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/handles/HandleSmall.svg"
+dest_files=["res://.godot/imported/HandleSmall.svg-810fdec38a338826427b31eec34e909e.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/visual/icons/handles/HandleSmallHovered.svg b/visual/icons/handles/HandleSmallHovered.svg
new file mode 100644
index 000000000..858ea96c1
--- /dev/null
+++ b/visual/icons/handles/HandleSmallHovered.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/handles/HandleSmallHovered.svg.import b/visual/icons/handles/HandleSmallHovered.svg.import
new file mode 100644
index 000000000..139645f08
--- /dev/null
+++ b/visual/icons/handles/HandleSmallHovered.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dtxhlaig2262a"
+path="res://.godot/imported/HandleSmallHovered.svg-0adadfb99fd8698ea0afb0d6d80c0bd4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/handles/HandleSmallHovered.svg"
+dest_files=["res://.godot/imported/HandleSmallHovered.svg-0adadfb99fd8698ea0afb0d6d80c0bd4.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/visual/icons/handles/HandleSmallHoveredSelected.svg b/visual/icons/handles/HandleSmallHoveredSelected.svg
new file mode 100644
index 000000000..868dfe5d9
--- /dev/null
+++ b/visual/icons/handles/HandleSmallHoveredSelected.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/handles/HandleSmallHoveredSelected.svg.import b/visual/icons/handles/HandleSmallHoveredSelected.svg.import
new file mode 100644
index 000000000..fd5fb7969
--- /dev/null
+++ b/visual/icons/handles/HandleSmallHoveredSelected.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b7kf00ki32vpo"
+path="res://.godot/imported/HandleSmallHoveredSelected.svg-42a108ede4755bee736a8cc7383dc941.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/handles/HandleSmallHoveredSelected.svg"
+dest_files=["res://.godot/imported/HandleSmallHoveredSelected.svg-42a108ede4755bee736a8cc7383dc941.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/visual/icons/handles/HandleSmallSelected.svg b/visual/icons/handles/HandleSmallSelected.svg
new file mode 100644
index 000000000..c5df2c65a
--- /dev/null
+++ b/visual/icons/handles/HandleSmallSelected.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/handles/HandleSmallSelected.svg.import b/visual/icons/handles/HandleSmallSelected.svg.import
new file mode 100644
index 000000000..ee0b28337
--- /dev/null
+++ b/visual/icons/handles/HandleSmallSelected.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://1wjhiy6xgebr"
+path="res://.godot/imported/HandleSmallSelected.svg-f982f4a1708b51c6f0f014b26586694c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/handles/HandleSmallSelected.svg"
+dest_files=["res://.godot/imported/HandleSmallSelected.svg-f982f4a1708b51c6f0f014b26586694c.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/visual/icons/tag/circle.svg b/visual/icons/tag/circle.svg
new file mode 100644
index 000000000..f9edc2936
--- /dev/null
+++ b/visual/icons/tag/circle.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/tag/circle.svg.import b/visual/icons/tag/circle.svg.import
new file mode 100644
index 000000000..a83a31ee3
--- /dev/null
+++ b/visual/icons/tag/circle.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://i3nv3rexgpfx"
+path="res://.godot/imported/circle.svg-467060837362b87abb0514abba0812b5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/circle.svg"
+dest_files=["res://.godot/imported/circle.svg-467060837362b87abb0514abba0812b5.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/visual/icons/tag/ellipse.svg b/visual/icons/tag/ellipse.svg
new file mode 100644
index 000000000..1c4e67779
--- /dev/null
+++ b/visual/icons/tag/ellipse.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/tag/ellipse.svg.import b/visual/icons/tag/ellipse.svg.import
new file mode 100644
index 000000000..dff9deeb4
--- /dev/null
+++ b/visual/icons/tag/ellipse.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c1atigsxatxdg"
+path="res://.godot/imported/ellipse.svg-b8b480293f0c9d2d3f27b92078723b6d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/ellipse.svg"
+dest_files=["res://.godot/imported/ellipse.svg-b8b480293f0c9d2d3f27b92078723b6d.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/visual/icons/tag/g.svg b/visual/icons/tag/g.svg
new file mode 100644
index 000000000..02f33f54c
--- /dev/null
+++ b/visual/icons/tag/g.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/tag/g.svg.import b/visual/icons/tag/g.svg.import
new file mode 100644
index 000000000..43f393bfc
--- /dev/null
+++ b/visual/icons/tag/g.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://v4diytxx43vs"
+path="res://.godot/imported/g.svg-ab7bc64ba1a21529757498010411b71e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/g.svg"
+dest_files=["res://.godot/imported/g.svg-ab7bc64ba1a21529757498010411b71e.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/visual/icons/tag/line.svg b/visual/icons/tag/line.svg
new file mode 100644
index 000000000..d9b5e2a62
--- /dev/null
+++ b/visual/icons/tag/line.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/tag/line.svg.import b/visual/icons/tag/line.svg.import
new file mode 100644
index 000000000..33092f0b9
--- /dev/null
+++ b/visual/icons/tag/line.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dckywhfjhuxke"
+path="res://.godot/imported/line.svg-303aee9c844ebaab408de974a1b4db06.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/line.svg"
+dest_files=["res://.godot/imported/line.svg-303aee9c844ebaab408de974a1b4db06.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/visual/icons/tag/linearGradient.svg b/visual/icons/tag/linearGradient.svg
new file mode 100644
index 000000000..14ce71f67
--- /dev/null
+++ b/visual/icons/tag/linearGradient.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/tag/linearGradient.svg.import b/visual/icons/tag/linearGradient.svg.import
new file mode 100644
index 000000000..e85e9ff5a
--- /dev/null
+++ b/visual/icons/tag/linearGradient.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ld6hoxtl7q6y"
+path="res://.godot/imported/linearGradient.svg-abec7a2d0e886794114aeb8930fd6a61.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/linearGradient.svg"
+dest_files=["res://.godot/imported/linearGradient.svg-abec7a2d0e886794114aeb8930fd6a61.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/visual/icons/tag/path.svg b/visual/icons/tag/path.svg
new file mode 100644
index 000000000..2fed28763
--- /dev/null
+++ b/visual/icons/tag/path.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/tag/path.svg.import b/visual/icons/tag/path.svg.import
new file mode 100644
index 000000000..b7819abe8
--- /dev/null
+++ b/visual/icons/tag/path.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dtogqqow1jfum"
+path="res://.godot/imported/path.svg-e44f8c74b13cc0fe39328e44b8ad7fd8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/path.svg"
+dest_files=["res://.godot/imported/path.svg-e44f8c74b13cc0fe39328e44b8ad7fd8.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/visual/icons/tag/radialGradient.svg b/visual/icons/tag/radialGradient.svg
new file mode 100644
index 000000000..f3e1c8cb1
--- /dev/null
+++ b/visual/icons/tag/radialGradient.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/tag/radialGradient.svg.import b/visual/icons/tag/radialGradient.svg.import
new file mode 100644
index 000000000..81ea25ed5
--- /dev/null
+++ b/visual/icons/tag/radialGradient.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://m0bt2nyq6axl"
+path="res://.godot/imported/radialGradient.svg-0482da4f7df90fb8cf6d10a5ec1946ab.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/radialGradient.svg"
+dest_files=["res://.godot/imported/radialGradient.svg-0482da4f7df90fb8cf6d10a5ec1946ab.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/visual/icons/tag/rect.svg b/visual/icons/tag/rect.svg
new file mode 100644
index 000000000..11ed6c5c4
--- /dev/null
+++ b/visual/icons/tag/rect.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/tag/rect.svg.import b/visual/icons/tag/rect.svg.import
new file mode 100644
index 000000000..5fc866b11
--- /dev/null
+++ b/visual/icons/tag/rect.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d32xg5svmjkb5"
+path="res://.godot/imported/rect.svg-500d18db7621c543f273f18cd19ab9d1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/rect.svg"
+dest_files=["res://.godot/imported/rect.svg-500d18db7621c543f273f18cd19ab9d1.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/visual/icons/tag/stop.svg b/visual/icons/tag/stop.svg
new file mode 100644
index 000000000..afaa5fb7a
--- /dev/null
+++ b/visual/icons/tag/stop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/tag/stop.svg.import b/visual/icons/tag/stop.svg.import
new file mode 100644
index 000000000..16786f632
--- /dev/null
+++ b/visual/icons/tag/stop.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cpict35mitaab"
+path="res://.godot/imported/stop.svg-115044a40a6a46ad230f05c3d58e2900.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/stop.svg"
+dest_files=["res://.godot/imported/stop.svg-115044a40a6a46ad230f05c3d58e2900.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/visual/icons/tag/unknown.svg b/visual/icons/tag/unknown.svg
new file mode 100644
index 000000000..97b8fb4e3
--- /dev/null
+++ b/visual/icons/tag/unknown.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/tag/unknown.svg.import b/visual/icons/tag/unknown.svg.import
new file mode 100644
index 000000000..36b1fd943
--- /dev/null
+++ b/visual/icons/tag/unknown.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://brua6wag257kc"
+path="res://.godot/imported/unknown.svg-5ab2cc84835c40cff0c8541c05d0d795.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/tag/unknown.svg"
+dest_files=["res://.godot/imported/unknown.svg-5ab2cc84835c40cff0c8541c05d0d795.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/visual/icons/theme/FolderUp.svg b/visual/icons/theme/FolderUp.svg
new file mode 100644
index 000000000..d798edf8c
--- /dev/null
+++ b/visual/icons/theme/FolderUp.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/FolderUp.svg.import b/visual/icons/theme/FolderUp.svg.import
new file mode 100644
index 000000000..8f80bf9d8
--- /dev/null
+++ b/visual/icons/theme/FolderUp.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://rrhdja8l17cn"
+path="res://.godot/imported/FolderUp.svg-b4eb7c260434a037353caaa49dc2e4f8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/FolderUp.svg"
+dest_files=["res://.godot/imported/FolderUp.svg-b4eb7c260434a037353caaa49dc2e4f8.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/visual/icons/theme/GuiBoxChecked.svg b/visual/icons/theme/GuiBoxChecked.svg
new file mode 100644
index 000000000..bb4989817
--- /dev/null
+++ b/visual/icons/theme/GuiBoxChecked.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/GuiBoxChecked.svg.import b/visual/icons/theme/GuiBoxChecked.svg.import
new file mode 100644
index 000000000..2a524c520
--- /dev/null
+++ b/visual/icons/theme/GuiBoxChecked.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://wdrpwa7gwmg"
+path="res://.godot/imported/GuiBoxChecked.svg-72c72039dbc1e536f6e371fb8146583d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/GuiBoxChecked.svg"
+dest_files=["res://.godot/imported/GuiBoxChecked.svg-72c72039dbc1e536f6e371fb8146583d.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/visual/icons/theme/GuiBoxCheckedDisabled.svg b/visual/icons/theme/GuiBoxCheckedDisabled.svg
new file mode 100644
index 000000000..f37ca8cd9
--- /dev/null
+++ b/visual/icons/theme/GuiBoxCheckedDisabled.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/GuiBoxCheckedDisabled.svg.import b/visual/icons/theme/GuiBoxCheckedDisabled.svg.import
new file mode 100644
index 000000000..6fc477402
--- /dev/null
+++ b/visual/icons/theme/GuiBoxCheckedDisabled.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d07xfjdtgtvop"
+path="res://.godot/imported/GuiBoxCheckedDisabled.svg-512d4bfc03a442881dcc2e48ad574478.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/GuiBoxCheckedDisabled.svg"
+dest_files=["res://.godot/imported/GuiBoxCheckedDisabled.svg-512d4bfc03a442881dcc2e48ad574478.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/visual/icons/theme/GuiBoxUnchecked.svg b/visual/icons/theme/GuiBoxUnchecked.svg
new file mode 100644
index 000000000..76e3b2b46
--- /dev/null
+++ b/visual/icons/theme/GuiBoxUnchecked.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/GuiBoxUnchecked.svg.import b/visual/icons/theme/GuiBoxUnchecked.svg.import
new file mode 100644
index 000000000..34f028b07
--- /dev/null
+++ b/visual/icons/theme/GuiBoxUnchecked.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d3a3xgsb8klyk"
+path="res://.godot/imported/GuiBoxUnchecked.svg-d53b47a39110bf333339d2803d03d549.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/GuiBoxUnchecked.svg"
+dest_files=["res://.godot/imported/GuiBoxUnchecked.svg-d53b47a39110bf333339d2803d03d549.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/visual/icons/theme/GuiBoxUncheckedDisabled.svg b/visual/icons/theme/GuiBoxUncheckedDisabled.svg
new file mode 100644
index 000000000..5865f53a2
--- /dev/null
+++ b/visual/icons/theme/GuiBoxUncheckedDisabled.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/GuiBoxUncheckedDisabled.svg.import b/visual/icons/theme/GuiBoxUncheckedDisabled.svg.import
new file mode 100644
index 000000000..1997dc65e
--- /dev/null
+++ b/visual/icons/theme/GuiBoxUncheckedDisabled.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://xbycstdv7g3m"
+path="res://.godot/imported/GuiBoxUncheckedDisabled.svg-d36e9a6981fb9dde0e9f6d2018ee1708.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/GuiBoxUncheckedDisabled.svg"
+dest_files=["res://.godot/imported/GuiBoxUncheckedDisabled.svg-d36e9a6981fb9dde0e9f6d2018ee1708.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/visual/icons/theme/GuiToggleChecked.svg b/visual/icons/theme/GuiToggleChecked.svg
new file mode 100644
index 000000000..3a96e9152
--- /dev/null
+++ b/visual/icons/theme/GuiToggleChecked.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/GuiToggleChecked.svg.import b/visual/icons/theme/GuiToggleChecked.svg.import
new file mode 100644
index 000000000..3fda897dd
--- /dev/null
+++ b/visual/icons/theme/GuiToggleChecked.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dwlr4bgptgwho"
+path="res://.godot/imported/GuiToggleChecked.svg-f91b95d5018255c5664dd8770df65181.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/GuiToggleChecked.svg"
+dest_files=["res://.godot/imported/GuiToggleChecked.svg-f91b95d5018255c5664dd8770df65181.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/visual/icons/theme/GuiToggleUnchecked.svg b/visual/icons/theme/GuiToggleUnchecked.svg
new file mode 100644
index 000000000..231edabe4
--- /dev/null
+++ b/visual/icons/theme/GuiToggleUnchecked.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/GuiToggleUnchecked.svg.import b/visual/icons/theme/GuiToggleUnchecked.svg.import
new file mode 100644
index 000000000..f4b4de100
--- /dev/null
+++ b/visual/icons/theme/GuiToggleUnchecked.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c12tg3dnydily"
+path="res://.godot/imported/GuiToggleUnchecked.svg-d89f39a367ae50fcc380cd5262331cf3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/GuiToggleUnchecked.svg"
+dest_files=["res://.godot/imported/GuiToggleUnchecked.svg-d89f39a367ae50fcc380cd5262331cf3.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/visual/icons/theme/SplitGrabber.svg b/visual/icons/theme/SplitGrabber.svg
new file mode 100644
index 000000000..7b0f65b32
--- /dev/null
+++ b/visual/icons/theme/SplitGrabber.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/SplitGrabber.svg.import b/visual/icons/theme/SplitGrabber.svg.import
new file mode 100644
index 000000000..9d0c35a31
--- /dev/null
+++ b/visual/icons/theme/SplitGrabber.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cnig3sejwsrf3"
+path="res://.godot/imported/SplitGrabber.svg-1251102e754f1f37eee20b4e69a24890.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/SplitGrabber.svg"
+dest_files=["res://.godot/imported/SplitGrabber.svg-1251102e754f1f37eee20b4e69a24890.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/visual/icons/theme/SplitGrabber2.svg b/visual/icons/theme/SplitGrabber2.svg
new file mode 100644
index 000000000..2fa615e13
--- /dev/null
+++ b/visual/icons/theme/SplitGrabber2.svg
@@ -0,0 +1 @@
+
diff --git a/visual/icons/theme/SplitGrabber2.svg.import b/visual/icons/theme/SplitGrabber2.svg.import
new file mode 100644
index 000000000..34d5bd246
--- /dev/null
+++ b/visual/icons/theme/SplitGrabber2.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://co75w07yqmcro"
+path="res://.godot/imported/SplitGrabber2.svg-18d40ab670e2b5a72dfeedf569d260c4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/theme/SplitGrabber2.svg"
+dest_files=["res://.godot/imported/SplitGrabber2.svg-18d40ab670e2b5a72dfeedf569d260c4.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/visual/main_theme.tres b/visual/main_theme.tres
index 92c6a3401..39f8c77f1 100644
--- a/visual/main_theme.tres
+++ b/visual/main_theme.tres
@@ -1,187 +1,916 @@
-[gd_resource type="Theme" load_steps=16 format=3 uid="uid://der6fnkliqqto"]
+[gd_resource type="Theme" load_steps=85 format=3 uid="uid://der6fnkliqqto"]
-[ext_resource type="FontFile" uid="uid://c7ury252fql35" path="res://visual/CodeFont.ttf" id="1_ulxqy"]
-[ext_resource type="FontFile" uid="uid://bd8rlfqek8f8u" path="res://visual/Font.ttf" id="2_d11o4"]
+[ext_resource type="Texture2D" uid="uid://wdrpwa7gwmg" path="res://visual/icons/theme/GuiBoxChecked.svg" id="1_agkhv"]
+[ext_resource type="Texture2D" uid="uid://d07xfjdtgtvop" path="res://visual/icons/theme/GuiBoxCheckedDisabled.svg" id="2_kfk3n"]
+[ext_resource type="Texture2D" uid="uid://d3a3xgsb8klyk" path="res://visual/icons/theme/GuiBoxUnchecked.svg" id="2_xwib4"]
+[ext_resource type="Texture2D" uid="uid://dwlr4bgptgwho" path="res://visual/icons/theme/GuiToggleChecked.svg" id="3_ggukc"]
+[ext_resource type="Texture2D" uid="uid://rrhdja8l17cn" path="res://visual/icons/theme/FolderUp.svg" id="4_1t13q"]
+[ext_resource type="Texture2D" uid="uid://cvh3kwbucf2n1" path="res://visual/icons/Reload.svg" id="4_8qjet"]
+[ext_resource type="Texture2D" uid="uid://kkxyv1gyrjgj" path="res://visual/icons/Visuals.svg" id="4_j0oku"]
+[ext_resource type="Texture2D" uid="uid://xbycstdv7g3m" path="res://visual/icons/theme/GuiBoxUncheckedDisabled.svg" id="4_t3o2j"]
+[ext_resource type="Texture2D" uid="uid://c12tg3dnydily" path="res://visual/icons/theme/GuiToggleUnchecked.svg" id="4_w667w"]
+[ext_resource type="Texture2D" uid="uid://cnig3sejwsrf3" path="res://visual/icons/theme/SplitGrabber.svg" id="5_7sidk"]
+[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="5_nfqug"]
+[ext_resource type="FontFile" uid="uid://dc0w4sx0h0fui" path="res://visual/fonts/FontBold.ttf" id="9_ugiac"]
+[ext_resource type="FontFile" uid="uid://clpf84p1lfwlp" path="res://visual/fonts/Font.ttf" id="11_ccloq"]
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6glqg"]
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i4np8"]
+content_margin_left = 12.0
+content_margin_top = 8.0
+content_margin_right = 12.0
+content_margin_bottom = 8.0
+bg_color = Color(0.129412, 0.14902, 0.207843, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.24, 0.28, 0.4, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+corner_detail = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4tonr"]
+content_margin_left = 6.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+bg_color = Color(0.0352941, 0.0352941, 0.0509804, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ay4gh"]
+bg_color = Color(1, 1, 1, 0.0666667)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.4, 0.8, 1, 0.8)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h5umu"]
+content_margin_left = 6.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+bg_color = Color(0.137255, 0.156863, 0.25098, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.262745, 0.337255, 0.478431, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yq2fk"]
+content_margin_left = 6.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+bg_color = Color(0.109804, 0.117647, 0.219608, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.192157, 0.219608, 0.34902, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gnyjn"]
+content_margin_left = 6.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+bg_color = Color(0.239216, 0.329412, 0.6, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.376471, 0.560784, 0.74902, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cnkik"]
+content_margin_left = 4.0
+content_margin_top = 2.0
+content_margin_right = 4.0
+content_margin_bottom = 2.0
+bg_color = Color(0.0196078, 0.0235294, 0.027451, 0.333333)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ehixj"]
+content_margin_left = 4.0
+content_margin_top = 2.0
+content_margin_right = 4.0
+content_margin_bottom = 2.0
+bg_color = Color(0.866667, 0.933333, 1, 0.0666667)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xs6y6"]
+content_margin_left = 4.0
+content_margin_top = 2.0
+content_margin_right = 4.0
+content_margin_bottom = 2.0
+bg_color = Color(0.866667, 0.933333, 1, 0.0666667)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_kja1p"]
+content_margin_left = 4.0
+content_margin_top = 2.0
+content_margin_right = 4.0
+content_margin_bottom = 2.0
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_qwstu"]
+content_margin_left = 4.0
+content_margin_top = 2.0
+content_margin_right = 4.0
+content_margin_bottom = 2.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bi87s"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.0196078, 0.0235294, 0.027451, 0.333333)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ky4q6"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.866667, 0.933333, 1, 0.0666667)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_q84qj"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5jayv"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.866667, 0.933333, 1, 0.2)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_p5ses"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_idfeh"]
+bg_color = Color(0.0627451, 0.0627451, 0.101961, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+expand_margin_top = 2.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v7lev"]
+content_margin_top = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.203922, 0.254902, 0.4, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_n2br6"]
+content_margin_top = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.275, 0.335, 0.5, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nhrif"]
+content_margin_top = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.376471, 0.560784, 0.74902, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k0vk4"]
+content_margin_top = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.06, 0.06, 0.1, 0.6)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxLine" id="StyleBoxLine_f2kuo"]
+color = Color(0.254902, 0.254902, 0.34902, 1)
+thickness = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qepg8"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
-bg_color = Color(0.0588235, 0.0588235, 0.0784314, 1)
+bg_color = Color(0.0352941, 0.0352941, 0.0509804, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
-border_color = Color(0.121569, 0.121569, 0.14902, 1)
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
-corner_detail = 16
-
-[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_gu1eq"]
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h5umu"]
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c4o10"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
-bg_color = Color(0.012, 0.012, 0.2, 1)
+bg_color = Color(0.137255, 0.156863, 0.25098, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
-border_color = Color(0.08, 0.16, 0.4, 1)
+border_color = Color(0.262745, 0.337255, 0.478431, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
-corner_detail = 16
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yq2fk"]
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ecs6u"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
-bg_color = Color(0.00784314, 0.00784314, 0.14902, 1)
+bg_color = Color(0.121569, 0.129412, 0.219608, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
-border_color = Color(0.057, 0.11775, 0.3, 1)
+border_color = Color(0.192157, 0.219608, 0.34902, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
-corner_detail = 16
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gnyjn"]
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ye6kl"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
-bg_color = Color(0.27, 0.4275, 0.9, 1)
+bg_color = Color(0.239216, 0.329412, 0.6, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
-border_color = Color(0.6, 0.8, 1, 1)
+border_color = Color(0.376471, 0.560784, 0.74902, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
-corner_detail = 16
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uk6li"]
-bg_color = Color(0, 0, 0, 0)
-border_width_left = 2
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8vw7r"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 5.0
+content_margin_bottom = 4.0
+bg_color = Color(0.0352941, 0.0352941, 0.0509804, 1)
+border_width_left = 1
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
-border_color = Color(0.6, 0.8, 1, 1)
-corner_radius_top_left = 1
-corner_radius_top_right = 1
-corner_radius_bottom_right = 1
-corner_radius_bottom_left = 1
-corner_detail = 1
-expand_margin_left = 2.0
-expand_margin_top = 2.0
-expand_margin_right = 2.0
-expand_margin_bottom = 2.0
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rcthl"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 5.0
+content_margin_bottom = 4.0
+bg_color = Color(0.0941176, 0.0941176, 0.14902, 1)
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.227451, 0.227451, 0.301961, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2dyv3"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 5.0
+content_margin_bottom = 4.0
+bg_color = Color(0.0627451, 0.0627451, 0.101961, 1)
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.152941, 0.152941, 0.2, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_m8uy4"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 5.0
+content_margin_bottom = 4.0
+bg_color = Color(0.192157, 0.207843, 0.34902, 1)
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.329412, 0.403922, 0.54902, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_x2qq7"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+draw_center = false
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_haqea"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+draw_center = false
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.227451, 0.227451, 0.301961, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rqjin"]
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ms2eo"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+draw_center = false
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.152941, 0.152941, 0.2, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_o3p5c"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+draw_center = false
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.329412, 0.403922, 0.54902, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k1x4s"]
content_margin_left = 5.0
-bg_color = Color(0.062, 0.062, 0.1, 1)
-border_width_left = 2
+bg_color = Color(0.0627451, 0.0627451, 0.101961, 1)
+border_width_left = 1
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
-border_color = Color(0.1368, 0.1368, 0.18, 1)
-corner_radius_top_left = 6
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_radius_bottom_left = 6
-corner_detail = 16
+border_color = Color(0.152, 0.152, 0.2, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ff1pj"]
+content_margin_left = 5.0
+bg_color = Color(0.0352941, 0.0352941, 0.0509804, 1)
+border_width_left = 1
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ri0uc"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_730xo"]
content_margin_left = 5.0
-bg_color = Color(0.062, 0.062, 0.1, 1)
+bg_color = Color(0.0627451, 0.0627451, 0.101961, 1)
border_width_left = 2
border_width_top = 2
-border_width_right = 1
+border_width_right = 2
border_width_bottom = 2
-border_color = Color(0.1368, 0.1368, 0.18, 1)
-corner_radius_top_left = 6
-corner_radius_bottom_left = 6
-corner_detail = 16
+border_color = Color(0.152941, 0.152941, 0.2, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_efvcd"]
+content_margin_left = 5.0
+bg_color = Color(0.0352941, 0.0352941, 0.0509804, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_rmu68"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_iof82"]
+content_margin_left = 4.0
+content_margin_bottom = 0.0
+bg_color = Color(0.0627451, 0.0627451, 0.101961, 1)
+border_width_bottom = 2
+border_color = Color(0.228, 0.228, 0.3, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c4urh"]
+content_margin_left = 4.0
+content_margin_bottom = 0.0
+bg_color = Color(0.0352941, 0.0352941, 0.0509804, 1)
+border_width_bottom = 2
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c2si1"]
content_margin_left = 2.0
content_margin_right = 2.0
-bg_color = Color(0.0975, 0.0975, 0.15, 1)
+bg_color = Color(0.0980392, 0.0980392, 0.14902, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
-border_color = Color(0.1875, 0.1875, 0.25, 1)
-corner_radius_top_left = 6
-corner_radius_top_right = 6
-corner_radius_bottom_right = 6
-corner_radius_bottom_left = 6
-corner_detail = 16
+border_color = Color(0.254902, 0.254902, 0.34902, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dptuh"]
-bg_color = Color(0.24, 0.33, 0.6, 1)
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8bmk3"]
+content_margin_left = 5.0
+content_margin_top = 4.0
+content_margin_right = 3.0
+content_margin_bottom = 4.0
+bg_color = Color(0.0352941, 0.0352941, 0.0509804, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v2cqs"]
+content_margin_left = 5.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.137255, 0.156863, 0.25098, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.262745, 0.337255, 0.478431, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v3rbs"]
+content_margin_left = 5.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.109804, 0.117647, 0.219608, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.192157, 0.219608, 0.34902, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s605b"]
+content_margin_left = 5.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.239216, 0.329412, 0.6, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.376471, 0.560784, 0.74902, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i5hko"]
+content_margin_left = 5.0
+bg_color = Color(0.062, 0.062, 0.1, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.152941, 0.152941, 0.2, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fuf3i"]
+content_margin_left = 5.0
+bg_color = Color(0.0352941, 0.0352941, 0.0509804, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 1
+border_width_bottom = 2
+border_color = Color(0.0901961, 0.0901961, 0.101961, 1)
+corner_radius_top_left = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7qkcb"]
+content_margin_left = 6.0
+content_margin_top = 5.0
+content_margin_right = 6.0
+content_margin_bottom = 6.0
+bg_color = Color(0.078, 0.0828, 0.15, 1)
+border_width_left = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.14902, 0.164706, 0.301961, 1)
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_auw38"]
+content_margin_left = 12.0
+content_margin_top = 3.0
+content_margin_right = 12.0
+content_margin_bottom = 3.0
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_kohom"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_r2aev"]
+content_margin_left = 12.0
+content_margin_top = 3.0
+content_margin_right = 12.0
+content_margin_bottom = 3.0
+bg_color = Color(0.121569, 0.129412, 0.219608, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
-corner_radius_bottom_right = 4
-corner_radius_bottom_left = 4
-corner_detail = 16
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jo7ks"]
-bg_color = Color(0.35, 0.4375, 0.7, 1)
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nhytu"]
+content_margin_left = 12.0
+content_margin_top = 3.0
+content_margin_right = 12.0
+content_margin_bottom = 3.0
+bg_color = Color(0.16, 0.186667, 0.32, 1)
+border_width_top = 2
+border_color = Color(0.376471, 0.560784, 0.74902, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c7arg"]
+content_margin_left = 12.0
+content_margin_top = 3.0
+content_margin_right = 12.0
+content_margin_bottom = 3.0
+bg_color = Color(0.09, 0.0975, 0.18, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
-corner_radius_bottom_right = 4
-corner_radius_bottom_left = 4
-corner_detail = 16
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jd7jj"]
+content_margin_left = 0.0
+content_margin_top = 0.0
+content_margin_right = 0.0
+content_margin_bottom = 0.0
+bg_color = Color(0.0627451, 0.0627451, 0.101961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_usmkl"]
+content_margin_left = 2.0
+content_margin_right = 2.0
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_p0ubs"]
+content_margin_left = 2.0
+content_margin_right = 2.0
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_rx78m"]
+content_margin_left = 2.0
+content_margin_right = 2.0
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_sjt13"]
+content_margin_left = 2.0
+content_margin_right = 2.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1gwcd"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.2, 0.34902, 0.501961, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ichfn"]
+content_margin_left = 5.0
+bg_color = Color(0.0627451, 0.0627451, 0.101961, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.152941, 0.152941, 0.2, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ghiib"]
+content_margin_left = 6.0
+content_margin_top = 1.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+bg_color = Color(0.129412, 0.14902, 0.207843, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.24, 0.28, 0.4, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8ikvi"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.866667, 0.933333, 1, 0.133333)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fmd2e"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.866667, 0.933333, 1, 0.0666667)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1jlx6"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.866667, 0.933333, 1, 0.266667)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dptuh"]
+content_margin_top = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.203922, 0.254902, 0.4, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jo7ks"]
+content_margin_top = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.275, 0.335, 0.5, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k6fme"]
-bg_color = Color(0.48, 0.64, 0.8, 1)
-corner_radius_top_left = 4
-corner_radius_top_right = 4
-corner_radius_bottom_right = 4
-corner_radius_bottom_left = 4
-corner_detail = 16
+content_margin_top = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.376471, 0.560784, 0.74902, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wn4u7"]
+content_margin_left = 4.0
+content_margin_right = 4.0
+bg_color = Color(0.06, 0.06, 0.1, 0.6)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k5a4y"]
+content_margin_left = 2.0
+content_margin_right = 2.0
+bg_color = Color(0.0975, 0.0975, 0.15, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.1875, 0.1875, 0.25, 1)
+corner_radius_top_left = 6
+corner_radius_top_right = 6
+corner_radius_bottom_right = 6
+corner_radius_bottom_left = 6
+expand_margin_left = 8.0
+expand_margin_top = 32.0
+expand_margin_right = 8.0
+expand_margin_bottom = 8.0
[resource]
-default_font = ExtResource("2_d11o4")
-default_font_size = 16
-Button/styles/disabled = SubResource("StyleBoxFlat_6glqg")
-Button/styles/focus = SubResource("StyleBoxEmpty_gu1eq")
+default_font = ExtResource("11_ccloq")
+default_font_size = 13
+AcceptDialog/styles/panel = SubResource("StyleBoxFlat_i4np8")
+Button/constants/h_separation = 6
+Button/styles/disabled = SubResource("StyleBoxFlat_4tonr")
+Button/styles/focus = SubResource("StyleBoxFlat_ay4gh")
Button/styles/hover = SubResource("StyleBoxFlat_h5umu")
Button/styles/normal = SubResource("StyleBoxFlat_yq2fk")
Button/styles/pressed = SubResource("StyleBoxFlat_gnyjn")
-CodeEdit/colors/selection_color = Color(0.4, 0.54902, 1, 0.4)
-CodeEdit/font_sizes/font_size = 12
-CodeEdit/fonts/font = ExtResource("1_ulxqy")
-CodeEdit/styles/focus = SubResource("StyleBoxFlat_uk6li")
-CodeEdit/styles/read_only = SubResource("StyleBoxFlat_rqjin")
-Label/colors/font_color = Color(0.866667, 0.933333, 1, 1)
-Label/font_sizes/font_size = 20
-LineEdit/colors/caret_color = Color(0.95, 0.855, 0.855, 1)
+CheckBox/icons/checked = ExtResource("1_agkhv")
+CheckBox/icons/checked_disabled = ExtResource("2_kfk3n")
+CheckBox/icons/unchecked = ExtResource("2_xwib4")
+CheckBox/icons/unchecked_disabled = ExtResource("4_t3o2j")
+CheckBox/styles/disabled = SubResource("StyleBoxFlat_cnkik")
+CheckBox/styles/hover = SubResource("StyleBoxFlat_ehixj")
+CheckBox/styles/hover_pressed = SubResource("StyleBoxFlat_xs6y6")
+CheckBox/styles/normal = SubResource("StyleBoxEmpty_kja1p")
+CheckBox/styles/pressed = SubResource("StyleBoxEmpty_qwstu")
+CheckButton/icons/checked = ExtResource("3_ggukc")
+CheckButton/icons/unchecked = ExtResource("4_w667w")
+FileDialog/icons/parent_folder = ExtResource("4_1t13q")
+FileDialog/icons/reload = ExtResource("4_8qjet")
+FileDialog/icons/toggle_hidden = ExtResource("4_j0oku")
+FlatButton/base_type = &"Button"
+FlatButton/colors/icon_hover_color = Color(1, 1, 1, 1)
+FlatButton/colors/icon_normal_color = Color(0.74902, 0.74902, 0.74902, 1)
+FlatButton/colors/icon_pressed_color = Color(0.74902, 0.87451, 1, 1)
+FlatButton/styles/disabled = SubResource("StyleBoxFlat_bi87s")
+FlatButton/styles/hover = SubResource("StyleBoxFlat_ky4q6")
+FlatButton/styles/normal = SubResource("StyleBoxEmpty_q84qj")
+FlatButton/styles/pressed = SubResource("StyleBoxFlat_5jayv")
+GoodColorPickerLineEdit/base_type = &"LineEdit"
+GoodColorPickerLineEdit/font_sizes/font_size = 11
+GoodColorPickerLineEdit/fonts/font = ExtResource("5_nfqug")
+GoodColorPickerLineEdit/styles/focus = SubResource("StyleBoxEmpty_p5ses")
+GoodColorPickerLineEdit/styles/normal = SubResource("StyleBoxFlat_idfeh")
+HScrollBar/styles/grabber = SubResource("StyleBoxFlat_v7lev")
+HScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_n2br6")
+HScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_nhrif")
+HScrollBar/styles/scroll = SubResource("StyleBoxFlat_k0vk4")
+HSeparator/styles/separator = SubResource("StyleBoxLine_f2kuo")
+IconButton/base_type = &"Button"
+IconButton/styles/disabled = SubResource("StyleBoxFlat_qepg8")
+IconButton/styles/hover = SubResource("StyleBoxFlat_c4o10")
+IconButton/styles/normal = SubResource("StyleBoxFlat_ecs6u")
+IconButton/styles/pressed = SubResource("StyleBoxFlat_ye6kl")
+Label/font_sizes/font_size = 15
+LeftConnectedButton/base_type = &"Button"
+LeftConnectedButton/colors/icon_hover_color = Color(1, 1, 1, 1)
+LeftConnectedButton/colors/icon_normal_color = Color(0.74902, 0.74902, 0.74902, 1)
+LeftConnectedButton/colors/icon_pressed_color = Color(0.74902, 0.87451, 1, 1)
+LeftConnectedButton/styles/disabled = SubResource("StyleBoxFlat_8vw7r")
+LeftConnectedButton/styles/hover = SubResource("StyleBoxFlat_rcthl")
+LeftConnectedButton/styles/normal = SubResource("StyleBoxFlat_2dyv3")
+LeftConnectedButton/styles/pressed = SubResource("StyleBoxFlat_m8uy4")
+LeftConnectedButtonTransparent/base_type = &"Button"
+LeftConnectedButtonTransparent/styles/disabled = SubResource("StyleBoxFlat_x2qq7")
+LeftConnectedButtonTransparent/styles/hover = SubResource("StyleBoxFlat_haqea")
+LeftConnectedButtonTransparent/styles/normal = SubResource("StyleBoxFlat_ms2eo")
+LeftConnectedButtonTransparent/styles/pressed = SubResource("StyleBoxFlat_o3p5c")
+LeftConnectedLineEdit/base_type = &"LineEdit"
+LeftConnectedLineEdit/font_sizes/font_size = 12
+LeftConnectedLineEdit/fonts/font = ExtResource("5_nfqug")
+LeftConnectedLineEdit/styles/normal = SubResource("StyleBoxFlat_k1x4s")
+LeftConnectedLineEdit/styles/read_only = SubResource("StyleBoxFlat_ff1pj")
+LineEdit/colors/caret_color = Color(0.866667, 0.933333, 1, 0.866667)
+LineEdit/colors/font_color = Color(0.866667, 0.933333, 1, 1)
+LineEdit/colors/font_placeholder_color = Color(1, 1, 1, 0.333333)
LineEdit/colors/selection_color = Color(0.4, 0.54902, 1, 0.4)
LineEdit/font_sizes/font_size = 12
-LineEdit/fonts/font = ExtResource("1_ulxqy")
+LineEdit/fonts/font = ExtResource("5_nfqug")
LineEdit/styles/focus = SubResource("StyleBoxEmpty_ri0uc")
LineEdit/styles/normal = SubResource("StyleBoxFlat_730xo")
+LineEdit/styles/read_only = SubResource("StyleBoxFlat_efvcd")
+MiniLineEdit/base_type = &"LineEdit"
+MiniLineEdit/colors/caret_color = Color(0.94902, 0.854902, 0.854902, 1)
+MiniLineEdit/colors/font_outline_color = Color(0.866667, 0.933333, 1, 1)
+MiniLineEdit/colors/selection_color = Color(0.4, 0.54902, 1, 0.4)
+MiniLineEdit/font_sizes/font_size = 10
+MiniLineEdit/fonts/font = ExtResource("5_nfqug")
+MiniLineEdit/styles/focus = SubResource("StyleBoxEmpty_rmu68")
+MiniLineEdit/styles/normal = SubResource("StyleBoxFlat_iof82")
+MiniLineEdit/styles/read_only = SubResource("StyleBoxFlat_c4urh")
PanelContainer/styles/panel = SubResource("StyleBoxFlat_c2si1")
RichTextLabel/colors/selection_color = Color(0.4, 0.55, 1, 0.4)
+RichTextLabel/fonts/bold_font = ExtResource("9_ugiac")
+RightConnectedButton/base_type = &"Button"
+RightConnectedButton/styles/disabled = SubResource("StyleBoxFlat_8bmk3")
+RightConnectedButton/styles/hover = SubResource("StyleBoxFlat_v2cqs")
+RightConnectedButton/styles/normal = SubResource("StyleBoxFlat_v3rbs")
+RightConnectedButton/styles/pressed = SubResource("StyleBoxFlat_s605b")
+RightConnectedLineEdit/base_type = &"LineEdit"
+RightConnectedLineEdit/font_sizes/font_size = 12
+RightConnectedLineEdit/fonts/font = ExtResource("5_nfqug")
+RightConnectedLineEdit/styles/normal = SubResource("StyleBoxFlat_i5hko")
+RightConnectedLineEdit/styles/read_only = SubResource("StyleBoxFlat_fuf3i")
+TabContainer/constants/side_margin = 0
+TabContainer/font_sizes/font_size = 14
+TabContainer/styles/panel = SubResource("StyleBoxFlat_7qkcb")
+TabContainer/styles/tab_disabled = SubResource("StyleBoxEmpty_auw38")
+TabContainer/styles/tab_focus = SubResource("StyleBoxEmpty_kohom")
+TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_r2aev")
+TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_nhytu")
+TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_c7arg")
+TabContainer/styles/tabbar_background = SubResource("StyleBoxFlat_jd7jj")
+TextButton/base_type = &"Button"
+TextButton/colors/font_color = Color(0.501961, 0.501961, 0.501961, 1)
+TextButton/colors/font_hover_color = Color(0.501961, 0.501961, 0.501961, 1)
+TextButton/colors/font_pressed_color = Color(0.866667, 0.933333, 1, 0.866667)
+TextButton/styles/disabled = SubResource("StyleBoxEmpty_usmkl")
+TextButton/styles/hover = SubResource("StyleBoxEmpty_p0ubs")
+TextButton/styles/normal = SubResource("StyleBoxEmpty_rx78m")
+TextButton/styles/pressed = SubResource("StyleBoxEmpty_sjt13")
+TextEdit/colors/caret_color = Color(0, 0, 0, 0)
+TextEdit/colors/selection_color = Color(0.4, 0.54902, 1, 0.4)
+TextEdit/font_sizes/font_size = 12
+TextEdit/fonts/font = ExtResource("5_nfqug")
+TextEdit/styles/focus = SubResource("StyleBoxFlat_1gwcd")
+TextEdit/styles/normal = SubResource("StyleBoxFlat_ichfn")
+TooltipLabel/colors/font_color = Color(0.866667, 0.933333, 1, 1)
+TooltipLabel/font_sizes/font_size = 14
+TooltipLabel/fonts/font = ExtResource("11_ccloq")
+TooltipPanel/styles/panel = SubResource("StyleBoxFlat_ghiib")
+TranslucentButton/base_type = &"Button"
+TranslucentButton/styles/hover = SubResource("StyleBoxFlat_8ikvi")
+TranslucentButton/styles/normal = SubResource("StyleBoxFlat_fmd2e")
+TranslucentButton/styles/pressed = SubResource("StyleBoxFlat_1jlx6")
VScrollBar/styles/grabber = SubResource("StyleBoxFlat_dptuh")
VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_jo7ks")
VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_k6fme")
-VScrollBar/styles/scroll = null
-VScrollBar/styles/scroll_focus = null
+VScrollBar/styles/scroll = SubResource("StyleBoxFlat_wn4u7")
+VSplitContainer/icons/grabber = ExtResource("5_7sidk")
+Window/styles/embedded_border = SubResource("StyleBoxFlat_k5a4y")
diff --git a/visual/splash.png b/visual/splash.png
new file mode 100644
index 000000000..49100104f
Binary files /dev/null and b/visual/splash.png differ
diff --git a/visual/splash.png.import b/visual/splash.png.import
new file mode 100644
index 000000000..3852703e1
--- /dev/null
+++ b/visual/splash.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bom8y28xu652f"
+path="res://.godot/imported/splash.png-2e737774984f5a274381090076e4098a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/splash.png"
+dest_files=["res://.godot/imported/splash.png-2e737774984f5a274381090076e4098a.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
diff --git a/visual/splash.svg b/visual/splash.svg
new file mode 100644
index 000000000..3c763663e
--- /dev/null
+++ b/visual/splash.svg
@@ -0,0 +1 @@
+
diff --git a/visual/splash.svg.import b/visual/splash.svg.import
new file mode 100644
index 000000000..76f6a06b2
--- /dev/null
+++ b/visual/splash.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://rhqnuun4e7cj"
+path="res://.godot/imported/splash.svg-aaef44ef3b35ccb214e050febfc889ae.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/splash.svg"
+dest_files=["res://.godot/imported/splash.svg-aaef44ef3b35ccb214e050febfc889ae.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/web-build/.gdignore b/web-build/.gdignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/web-build/GodSVG.apple-touch-icon.png b/web-build/GodSVG.apple-touch-icon.png
new file mode 100644
index 000000000..8a6a17ae0
Binary files /dev/null and b/web-build/GodSVG.apple-touch-icon.png differ
diff --git a/web-build/GodSVG.audio.worklet.js b/web-build/GodSVG.audio.worklet.js
new file mode 100644
index 000000000..89b581b3d
--- /dev/null
+++ b/web-build/GodSVG.audio.worklet.js
@@ -0,0 +1,213 @@
+/**************************************************************************/
+/* audio.worklet.js */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+class RingBuffer {
+ constructor(p_buffer, p_state, p_threads) {
+ this.buffer = p_buffer;
+ this.avail = p_state;
+ this.threads = p_threads;
+ this.rpos = 0;
+ this.wpos = 0;
+ }
+
+ data_left() {
+ return this.threads ? Atomics.load(this.avail, 0) : this.avail;
+ }
+
+ space_left() {
+ return this.buffer.length - this.data_left();
+ }
+
+ read(output) {
+ const size = this.buffer.length;
+ let from = 0;
+ let to_write = output.length;
+ if (this.rpos + to_write > size) {
+ const high = size - this.rpos;
+ output.set(this.buffer.subarray(this.rpos, size));
+ from = high;
+ to_write -= high;
+ this.rpos = 0;
+ }
+ if (to_write) {
+ output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+ }
+ this.rpos += to_write;
+ if (this.threads) {
+ Atomics.add(this.avail, 0, -output.length);
+ Atomics.notify(this.avail, 0);
+ } else {
+ this.avail -= output.length;
+ }
+ }
+
+ write(p_buffer) {
+ const to_write = p_buffer.length;
+ const mw = this.buffer.length - this.wpos;
+ if (mw >= to_write) {
+ this.buffer.set(p_buffer, this.wpos);
+ this.wpos += to_write;
+ if (mw === to_write) {
+ this.wpos = 0;
+ }
+ } else {
+ const high = p_buffer.subarray(0, mw);
+ const low = p_buffer.subarray(mw);
+ this.buffer.set(high, this.wpos);
+ this.buffer.set(low);
+ this.wpos = low.length;
+ }
+ if (this.threads) {
+ Atomics.add(this.avail, 0, to_write);
+ Atomics.notify(this.avail, 0);
+ } else {
+ this.avail += to_write;
+ }
+ }
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ this.threads = false;
+ this.running = true;
+ this.lock = null;
+ this.notifier = null;
+ this.output = null;
+ this.output_buffer = new Float32Array();
+ this.input = null;
+ this.input_buffer = new Float32Array();
+ this.port.onmessage = (event) => {
+ const cmd = event.data['cmd'];
+ const data = event.data['data'];
+ this.parse_message(cmd, data);
+ };
+ }
+
+ process_notify() {
+ if (this.notifier) {
+ Atomics.add(this.notifier, 0, 1);
+ Atomics.notify(this.notifier, 0);
+ }
+ }
+
+ parse_message(p_cmd, p_data) {
+ if (p_cmd === 'start' && p_data) {
+ const state = p_data[0];
+ let idx = 0;
+ this.threads = true;
+ this.lock = state.subarray(idx, ++idx);
+ this.notifier = state.subarray(idx, ++idx);
+ const avail_in = state.subarray(idx, ++idx);
+ const avail_out = state.subarray(idx, ++idx);
+ this.input = new RingBuffer(p_data[1], avail_in, true);
+ this.output = new RingBuffer(p_data[2], avail_out, true);
+ } else if (p_cmd === 'stop') {
+ this.running = false;
+ this.output = null;
+ this.input = null;
+ this.lock = null;
+ this.notifier = null;
+ } else if (p_cmd === 'start_nothreads') {
+ this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+ } else if (p_cmd === 'chunk') {
+ this.output.write(p_data);
+ }
+ }
+
+ static array_has_data(arr) {
+ return arr.length && arr[0].length && arr[0][0].length;
+ }
+
+ process(inputs, outputs, parameters) {
+ if (!this.running) {
+ return false; // Stop processing.
+ }
+ if (this.output === null) {
+ return true; // Not ready yet, keep processing.
+ }
+ const process_input = GodotProcessor.array_has_data(inputs);
+ if (process_input) {
+ const input = inputs[0];
+ const chunk = input[0].length * input.length;
+ if (this.input_buffer.length !== chunk) {
+ this.input_buffer = new Float32Array(chunk);
+ }
+ if (!this.threads) {
+ GodotProcessor.write_input(this.input_buffer, input);
+ this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+ } else if (this.input.space_left() >= chunk) {
+ GodotProcessor.write_input(this.input_buffer, input);
+ this.input.write(this.input_buffer);
+ } else {
+ this.port.postMessage('Input buffer is full! Skipping input frame.');
+ }
+ }
+ const process_output = GodotProcessor.array_has_data(outputs);
+ if (process_output) {
+ const output = outputs[0];
+ const chunk = output[0].length * output.length;
+ if (this.output_buffer.length !== chunk) {
+ this.output_buffer = new Float32Array(chunk);
+ }
+ if (this.output.data_left() >= chunk) {
+ this.output.read(this.output_buffer);
+ GodotProcessor.write_output(output, this.output_buffer);
+ if (!this.threads) {
+ this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+ }
+ } else {
+ this.port.postMessage('Output buffer has not enough frames! Skipping output frame.');
+ }
+ }
+ this.process_notify();
+ return true;
+ }
+
+ static write_output(dest, source) {
+ const channels = dest.length;
+ for (let ch = 0; ch < channels; ch++) {
+ for (let sample = 0; sample < dest[ch].length; sample++) {
+ dest[ch][sample] = source[sample * channels + ch];
+ }
+ }
+ }
+
+ static write_input(dest, source) {
+ const channels = source.length;
+ for (let ch = 0; ch < channels; ch++) {
+ for (let sample = 0; sample < source[ch].length; sample++) {
+ dest[sample * channels + ch] = source[ch][sample];
+ }
+ }
+ }
+}
+
+registerProcessor('godot-processor', GodotProcessor);
diff --git a/web-build/GodSVG.icon.png b/web-build/GodSVG.icon.png
new file mode 100644
index 000000000..e99a2cfbf
Binary files /dev/null and b/web-build/GodSVG.icon.png differ
diff --git a/web-build/GodSVG.js b/web-build/GodSVG.js
new file mode 100644
index 000000000..0019fc134
--- /dev/null
+++ b/web-build/GodSVG.js
@@ -0,0 +1,14671 @@
+
+var Godot = (() => {
+ var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
+
+ return (
+function(Godot) {
+ Godot = Godot || {};
+
+
+
+// Support for growable heap + pthreads, where the buffer may change, so JS views
+// must be updated.
+function GROWABLE_HEAP_I8() {
+ if (wasmMemory.buffer != buffer) {
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ }
+ return HEAP8;
+}
+function GROWABLE_HEAP_U8() {
+ if (wasmMemory.buffer != buffer) {
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ }
+ return HEAPU8;
+}
+function GROWABLE_HEAP_I16() {
+ if (wasmMemory.buffer != buffer) {
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ }
+ return HEAP16;
+}
+function GROWABLE_HEAP_U16() {
+ if (wasmMemory.buffer != buffer) {
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ }
+ return HEAPU16;
+}
+function GROWABLE_HEAP_I32() {
+ if (wasmMemory.buffer != buffer) {
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ }
+ return HEAP32;
+}
+function GROWABLE_HEAP_U32() {
+ if (wasmMemory.buffer != buffer) {
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ }
+ return HEAPU32;
+}
+function GROWABLE_HEAP_F32() {
+ if (wasmMemory.buffer != buffer) {
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ }
+ return HEAPF32;
+}
+function GROWABLE_HEAP_F64() {
+ if (wasmMemory.buffer != buffer) {
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ }
+ return HEAPF64;
+}
+
+var Module = typeof Godot != "undefined" ? Godot : {};
+
+var readyPromiseResolve, readyPromiseReject;
+
+Module["ready"] = new Promise(function(resolve, reject) {
+ readyPromiseResolve = resolve;
+ readyPromiseReject = reject;
+});
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "_main")) {
+ Object.defineProperty(Module["ready"], "_main", {
+ configurable: true,
+ get: function() {
+ abort("You are getting _main on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "_main", {
+ configurable: true,
+ set: function() {
+ abort("You are setting _main on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "__emscripten_thread_init")) {
+ Object.defineProperty(Module["ready"], "__emscripten_thread_init", {
+ configurable: true,
+ get: function() {
+ abort("You are getting __emscripten_thread_init on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "__emscripten_thread_init", {
+ configurable: true,
+ set: function() {
+ abort("You are setting __emscripten_thread_init on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "__emscripten_thread_exit")) {
+ Object.defineProperty(Module["ready"], "__emscripten_thread_exit", {
+ configurable: true,
+ get: function() {
+ abort("You are getting __emscripten_thread_exit on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "__emscripten_thread_exit", {
+ configurable: true,
+ set: function() {
+ abort("You are setting __emscripten_thread_exit on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "__emscripten_thread_crashed")) {
+ Object.defineProperty(Module["ready"], "__emscripten_thread_crashed", {
+ configurable: true,
+ get: function() {
+ abort("You are getting __emscripten_thread_crashed on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "__emscripten_thread_crashed", {
+ configurable: true,
+ set: function() {
+ abort("You are setting __emscripten_thread_crashed on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "_emscripten_tls_init")) {
+ Object.defineProperty(Module["ready"], "_emscripten_tls_init", {
+ configurable: true,
+ get: function() {
+ abort("You are getting _emscripten_tls_init on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "_emscripten_tls_init", {
+ configurable: true,
+ set: function() {
+ abort("You are setting _emscripten_tls_init on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "_emscripten_current_thread_process_queued_calls")) {
+ Object.defineProperty(Module["ready"], "_emscripten_current_thread_process_queued_calls", {
+ configurable: true,
+ get: function() {
+ abort("You are getting _emscripten_current_thread_process_queued_calls on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "_emscripten_current_thread_process_queued_calls", {
+ configurable: true,
+ set: function() {
+ abort("You are setting _emscripten_current_thread_process_queued_calls on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "_pthread_self")) {
+ Object.defineProperty(Module["ready"], "_pthread_self", {
+ configurable: true,
+ get: function() {
+ abort("You are getting _pthread_self on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "_pthread_self", {
+ configurable: true,
+ set: function() {
+ abort("You are setting _pthread_self on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "establishStackSpace")) {
+ Object.defineProperty(Module["ready"], "establishStackSpace", {
+ configurable: true,
+ get: function() {
+ abort("You are getting establishStackSpace on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "establishStackSpace", {
+ configurable: true,
+ set: function() {
+ abort("You are setting establishStackSpace on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "invokeEntryPoint")) {
+ Object.defineProperty(Module["ready"], "invokeEntryPoint", {
+ configurable: true,
+ get: function() {
+ abort("You are getting invokeEntryPoint on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "invokeEntryPoint", {
+ configurable: true,
+ set: function() {
+ abort("You are setting invokeEntryPoint on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "_emscripten_webgl_commit_frame")) {
+ Object.defineProperty(Module["ready"], "_emscripten_webgl_commit_frame", {
+ configurable: true,
+ get: function() {
+ abort("You are getting _emscripten_webgl_commit_frame on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "_emscripten_webgl_commit_frame", {
+ configurable: true,
+ set: function() {
+ abort("You are setting _emscripten_webgl_commit_frame on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "__Z14godot_web_mainiPPc")) {
+ Object.defineProperty(Module["ready"], "__Z14godot_web_mainiPPc", {
+ configurable: true,
+ get: function() {
+ abort("You are getting __Z14godot_web_mainiPPc on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "__Z14godot_web_mainiPPc", {
+ configurable: true,
+ set: function() {
+ abort("You are setting __Z14godot_web_mainiPPc on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "___stdio_exit")) {
+ Object.defineProperty(Module["ready"], "___stdio_exit", {
+ configurable: true,
+ get: function() {
+ abort("You are getting ___stdio_exit on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "___stdio_exit", {
+ configurable: true,
+ set: function() {
+ abort("You are setting ___stdio_exit on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module["ready"], "onRuntimeInitialized")) {
+ Object.defineProperty(Module["ready"], "onRuntimeInitialized", {
+ configurable: true,
+ get: function() {
+ abort("You are getting onRuntimeInitialized on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+ Object.defineProperty(Module["ready"], "onRuntimeInitialized", {
+ configurable: true,
+ set: function() {
+ abort("You are setting onRuntimeInitialized on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js");
+ }
+ });
+}
+
+var moduleOverrides = Object.assign({}, Module);
+
+var arguments_ = [];
+
+var thisProgram = "./this.program";
+
+var quit_ = (status, toThrow) => {
+ throw toThrow;
+};
+
+var ENVIRONMENT_IS_WEB = typeof window == "object";
+
+var ENVIRONMENT_IS_WORKER = typeof importScripts == "function";
+
+var ENVIRONMENT_IS_NODE = typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string";
+
+var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER;
+
+if (Module["ENVIRONMENT"]) {
+ throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -s ENVIRONMENT=web or -s ENVIRONMENT=node)");
+}
+
+var ENVIRONMENT_IS_PTHREAD = Module["ENVIRONMENT_IS_PTHREAD"] || false;
+
+var scriptDirectory = "";
+
+function locateFile(path) {
+ if (Module["locateFile"]) {
+ return Module["locateFile"](path, scriptDirectory);
+ }
+ return scriptDirectory + path;
+}
+
+var read_, readAsync, readBinary, setWindowTitle;
+
+function logExceptionOnExit(e) {
+ if (e instanceof ExitStatus) return;
+ let toLog = e;
+ if (e && typeof e == "object" && e.stack) {
+ toLog = [ e, e.stack ];
+ }
+ err("exiting due to exception: " + toLog);
+}
+
+if (ENVIRONMENT_IS_SHELL) {
+ if (typeof process == "object" && typeof require === "function" || typeof window == "object" || typeof importScripts == "function") throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");
+ if (typeof read != "undefined") {
+ read_ = function shell_read(f) {
+ return read(f);
+ };
+ }
+ readBinary = function readBinary(f) {
+ let data;
+ if (typeof readbuffer == "function") {
+ return new Uint8Array(readbuffer(f));
+ }
+ data = read(f, "binary");
+ assert(typeof data == "object");
+ return data;
+ };
+ readAsync = function readAsync(f, onload, onerror) {
+ setTimeout(() => onload(readBinary(f)), 0);
+ };
+ if (typeof scriptArgs != "undefined") {
+ arguments_ = scriptArgs;
+ } else if (typeof arguments != "undefined") {
+ arguments_ = arguments;
+ }
+ if (typeof quit == "function") {
+ quit_ = ((status, toThrow) => {
+ logExceptionOnExit(toThrow);
+ quit(status);
+ });
+ }
+ if (typeof print != "undefined") {
+ if (typeof console == "undefined") console = {};
+ console.log = print;
+ console.warn = console.error = typeof printErr != "undefined" ? printErr : print;
+ }
+} else if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) {
+ if (ENVIRONMENT_IS_WORKER) {
+ scriptDirectory = self.location.href;
+ } else if (typeof document != "undefined" && document.currentScript) {
+ scriptDirectory = document.currentScript.src;
+ }
+ if (_scriptDir) {
+ scriptDirectory = _scriptDir;
+ }
+ if (scriptDirectory.indexOf("blob:") !== 0) {
+ scriptDirectory = scriptDirectory.substr(0, scriptDirectory.replace(/[?#].*/, "").lastIndexOf("/") + 1);
+ } else {
+ scriptDirectory = "";
+ }
+ if (!(typeof window == "object" || typeof importScripts == "function")) throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");
+ {
+ read_ = (url => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, false);
+ xhr.send(null);
+ return xhr.responseText;
+ });
+ if (ENVIRONMENT_IS_WORKER) {
+ readBinary = (url => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, false);
+ xhr.responseType = "arraybuffer";
+ xhr.send(null);
+ return new Uint8Array(xhr.response);
+ });
+ }
+ readAsync = ((url, onload, onerror) => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = (() => {
+ if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
+ onload(xhr.response);
+ return;
+ }
+ onerror();
+ });
+ xhr.onerror = onerror;
+ xhr.send(null);
+ });
+ }
+ setWindowTitle = (title => document.title = title);
+} else {
+ throw new Error("environment detection error");
+}
+
+var out = Module["print"] || console.log.bind(console);
+
+var err = Module["printErr"] || console.warn.bind(console);
+
+Object.assign(Module, moduleOverrides);
+
+moduleOverrides = null;
+
+if (Module["arguments"]) arguments_ = Module["arguments"];
+
+if (!Object.getOwnPropertyDescriptor(Module, "arguments")) {
+ Object.defineProperty(Module, "arguments", {
+ configurable: true,
+ get: function() {
+ abort("Module.arguments has been replaced with plain arguments_ (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+if (Module["thisProgram"]) thisProgram = Module["thisProgram"];
+
+if (!Object.getOwnPropertyDescriptor(Module, "thisProgram")) {
+ Object.defineProperty(Module, "thisProgram", {
+ configurable: true,
+ get: function() {
+ abort("Module.thisProgram has been replaced with plain thisProgram (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+if (Module["quit"]) quit_ = Module["quit"];
+
+if (!Object.getOwnPropertyDescriptor(Module, "quit")) {
+ Object.defineProperty(Module, "quit", {
+ configurable: true,
+ get: function() {
+ abort("Module.quit has been replaced with plain quit_ (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+assert(typeof Module["memoryInitializerPrefixURL"] == "undefined", "Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead");
+
+assert(typeof Module["pthreadMainPrefixURL"] == "undefined", "Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead");
+
+assert(typeof Module["cdInitializerPrefixURL"] == "undefined", "Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead");
+
+assert(typeof Module["filePackagePrefixURL"] == "undefined", "Module.filePackagePrefixURL option was removed, use Module.locateFile instead");
+
+assert(typeof Module["read"] == "undefined", "Module.read option was removed (modify read_ in JS)");
+
+assert(typeof Module["readAsync"] == "undefined", "Module.readAsync option was removed (modify readAsync in JS)");
+
+assert(typeof Module["readBinary"] == "undefined", "Module.readBinary option was removed (modify readBinary in JS)");
+
+assert(typeof Module["setWindowTitle"] == "undefined", "Module.setWindowTitle option was removed (modify setWindowTitle in JS)");
+
+assert(typeof Module["TOTAL_MEMORY"] == "undefined", "Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY");
+
+if (!Object.getOwnPropertyDescriptor(Module, "read")) {
+ Object.defineProperty(Module, "read", {
+ configurable: true,
+ get: function() {
+ abort("Module.read has been replaced with plain read_ (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module, "readAsync")) {
+ Object.defineProperty(Module, "readAsync", {
+ configurable: true,
+ get: function() {
+ abort("Module.readAsync has been replaced with plain readAsync (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module, "readBinary")) {
+ Object.defineProperty(Module, "readBinary", {
+ configurable: true,
+ get: function() {
+ abort("Module.readBinary has been replaced with plain readBinary (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+if (!Object.getOwnPropertyDescriptor(Module, "setWindowTitle")) {
+ Object.defineProperty(Module, "setWindowTitle", {
+ configurable: true,
+ get: function() {
+ abort("Module.setWindowTitle has been replaced with plain setWindowTitle (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+var PROXYFS = "PROXYFS is no longer included by default; build with -lproxyfs.js";
+
+var WORKERFS = "WORKERFS is no longer included by default; build with -lworkerfs.js";
+
+var NODEFS = "NODEFS is no longer included by default; build with -lnodefs.js";
+
+assert(ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_NODE, "Pthreads do not work in this environment yet (need Web Workers, or an alternative to them)");
+
+assert(!ENVIRONMENT_IS_NODE, "node environment detected but not enabled at build time. Add 'node' to `-s ENVIRONMENT` to enable.");
+
+assert(!ENVIRONMENT_IS_SHELL, "shell environment detected but not enabled at build time. Add 'shell' to `-s ENVIRONMENT` to enable.");
+
+var STACK_ALIGN = 16;
+
+var POINTER_SIZE = 4;
+
+function getNativeTypeSize(type) {
+ switch (type) {
+ case "i1":
+ case "i8":
+ return 1;
+
+ case "i16":
+ return 2;
+
+ case "i32":
+ return 4;
+
+ case "i64":
+ return 8;
+
+ case "float":
+ return 4;
+
+ case "double":
+ return 8;
+
+ default:
+ {
+ if (type[type.length - 1] === "*") {
+ return POINTER_SIZE;
+ } else if (type[0] === "i") {
+ const bits = Number(type.substr(1));
+ assert(bits % 8 === 0, "getNativeTypeSize invalid bits " + bits + ", type " + type);
+ return bits / 8;
+ } else {
+ return 0;
+ }
+ }
+ }
+}
+
+function warnOnce(text) {
+ if (!warnOnce.shown) warnOnce.shown = {};
+ if (!warnOnce.shown[text]) {
+ warnOnce.shown[text] = 1;
+ err(text);
+ }
+}
+
+function convertJsFunctionToWasm(func, sig) {
+ if (typeof WebAssembly.Function == "function") {
+ var typeNames = {
+ "i": "i32",
+ "j": "i64",
+ "f": "f32",
+ "d": "f64"
+ };
+ var type = {
+ parameters: [],
+ results: sig[0] == "v" ? [] : [ typeNames[sig[0]] ]
+ };
+ for (var i = 1; i < sig.length; ++i) {
+ type.parameters.push(typeNames[sig[i]]);
+ }
+ return new WebAssembly.Function(type, func);
+ }
+ var typeSection = [ 1, 0, 1, 96 ];
+ var sigRet = sig.slice(0, 1);
+ var sigParam = sig.slice(1);
+ var typeCodes = {
+ "i": 127,
+ "j": 126,
+ "f": 125,
+ "d": 124
+ };
+ typeSection.push(sigParam.length);
+ for (var i = 0; i < sigParam.length; ++i) {
+ typeSection.push(typeCodes[sigParam[i]]);
+ }
+ if (sigRet == "v") {
+ typeSection.push(0);
+ } else {
+ typeSection = typeSection.concat([ 1, typeCodes[sigRet] ]);
+ }
+ typeSection[1] = typeSection.length - 2;
+ var bytes = new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0 ].concat(typeSection, [ 2, 7, 1, 1, 101, 1, 102, 0, 0, 7, 5, 1, 1, 102, 0, 0 ]));
+ var module = new WebAssembly.Module(bytes);
+ var instance = new WebAssembly.Instance(module, {
+ "e": {
+ "f": func
+ }
+ });
+ var wrappedFunc = instance.exports["f"];
+ return wrappedFunc;
+}
+
+var freeTableIndexes = [];
+
+var functionsInTableMap;
+
+function getEmptyTableSlot() {
+ if (freeTableIndexes.length) {
+ return freeTableIndexes.pop();
+ }
+ try {
+ wasmTable.grow(1);
+ } catch (err) {
+ if (!(err instanceof RangeError)) {
+ throw err;
+ }
+ throw "Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.";
+ }
+ return wasmTable.length - 1;
+}
+
+function updateTableMap(offset, count) {
+ for (var i = offset; i < offset + count; i++) {
+ var item = getWasmTableEntry(i);
+ if (item) {
+ functionsInTableMap.set(item, i);
+ }
+ }
+}
+
+function addFunction(func, sig) {
+ assert(typeof func != "undefined");
+ if (!functionsInTableMap) {
+ functionsInTableMap = new WeakMap();
+ updateTableMap(0, wasmTable.length);
+ }
+ if (functionsInTableMap.has(func)) {
+ return functionsInTableMap.get(func);
+ }
+ var ret = getEmptyTableSlot();
+ try {
+ setWasmTableEntry(ret, func);
+ } catch (err) {
+ if (!(err instanceof TypeError)) {
+ throw err;
+ }
+ assert(typeof sig != "undefined", "Missing signature argument to addFunction: " + func);
+ var wrapped = convertJsFunctionToWasm(func, sig);
+ setWasmTableEntry(ret, wrapped);
+ }
+ functionsInTableMap.set(func, ret);
+ return ret;
+}
+
+function removeFunction(index) {
+ functionsInTableMap.delete(getWasmTableEntry(index));
+ freeTableIndexes.push(index);
+}
+
+var tempRet0 = 0;
+
+var setTempRet0 = value => {
+ tempRet0 = value;
+};
+
+var getTempRet0 = () => tempRet0;
+
+var Atomics_load = Atomics.load;
+
+var Atomics_store = Atomics.store;
+
+var Atomics_compareExchange = Atomics.compareExchange;
+
+var wasmBinary;
+
+if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"];
+
+if (!Object.getOwnPropertyDescriptor(Module, "wasmBinary")) {
+ Object.defineProperty(Module, "wasmBinary", {
+ configurable: true,
+ get: function() {
+ abort("Module.wasmBinary has been replaced with plain wasmBinary (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+var noExitRuntime = Module["noExitRuntime"] || false;
+
+if (!Object.getOwnPropertyDescriptor(Module, "noExitRuntime")) {
+ Object.defineProperty(Module, "noExitRuntime", {
+ configurable: true,
+ get: function() {
+ abort("Module.noExitRuntime has been replaced with plain noExitRuntime (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+if (typeof WebAssembly != "object") {
+ abort("no native wasm support detected");
+}
+
+function setValue(ptr, value, type = "i8", noSafe) {
+ if (type.charAt(type.length - 1) === "*") type = "i32";
+ switch (type) {
+ case "i1":
+ GROWABLE_HEAP_I8()[ptr >> 0] = value;
+ break;
+
+ case "i8":
+ GROWABLE_HEAP_I8()[ptr >> 0] = value;
+ break;
+
+ case "i16":
+ GROWABLE_HEAP_I16()[ptr >> 1] = value;
+ break;
+
+ case "i32":
+ GROWABLE_HEAP_I32()[ptr >> 2] = value;
+ break;
+
+ case "i64":
+ tempI64 = [ value >>> 0, (tempDouble = value, +Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math.min(+Math.floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ],
+ GROWABLE_HEAP_I32()[ptr >> 2] = tempI64[0], GROWABLE_HEAP_I32()[ptr + 4 >> 2] = tempI64[1];
+ break;
+
+ case "float":
+ GROWABLE_HEAP_F32()[ptr >> 2] = value;
+ break;
+
+ case "double":
+ GROWABLE_HEAP_F64()[ptr >> 3] = value;
+ break;
+
+ default:
+ abort("invalid type for setValue: " + type);
+ }
+}
+
+function getValue(ptr, type = "i8", noSafe) {
+ if (type.charAt(type.length - 1) === "*") type = "i32";
+ switch (type) {
+ case "i1":
+ return GROWABLE_HEAP_I8()[ptr >> 0];
+
+ case "i8":
+ return GROWABLE_HEAP_I8()[ptr >> 0];
+
+ case "i16":
+ return GROWABLE_HEAP_I16()[ptr >> 1];
+
+ case "i32":
+ return GROWABLE_HEAP_I32()[ptr >> 2];
+
+ case "i64":
+ return GROWABLE_HEAP_I32()[ptr >> 2];
+
+ case "float":
+ return GROWABLE_HEAP_F32()[ptr >> 2];
+
+ case "double":
+ return Number(GROWABLE_HEAP_F64()[ptr >> 3]);
+
+ default:
+ abort("invalid type for getValue: " + type);
+ }
+ return null;
+}
+
+var wasmMemory;
+
+var wasmModule;
+
+var ABORT = false;
+
+var EXITSTATUS;
+
+function assert(condition, text) {
+ if (!condition) {
+ abort("Assertion failed" + (text ? ": " + text : ""));
+ }
+}
+
+function getCFunc(ident) {
+ var func = Module["_" + ident];
+ assert(func, "Cannot call unknown function " + ident + ", make sure it is exported");
+ return func;
+}
+
+function ccall(ident, returnType, argTypes, args, opts) {
+ var toC = {
+ "string": function(str) {
+ var ret = 0;
+ if (str !== null && str !== undefined && str !== 0) {
+ var len = (str.length << 2) + 1;
+ ret = stackAlloc(len);
+ stringToUTF8(str, ret, len);
+ }
+ return ret;
+ },
+ "array": function(arr) {
+ var ret = stackAlloc(arr.length);
+ writeArrayToMemory(arr, ret);
+ return ret;
+ }
+ };
+ function convertReturnValue(ret) {
+ if (returnType === "string") return UTF8ToString(ret);
+ if (returnType === "boolean") return Boolean(ret);
+ return ret;
+ }
+ var func = getCFunc(ident);
+ var cArgs = [];
+ var stack = 0;
+ assert(returnType !== "array", 'Return type should not be "array".');
+ if (args) {
+ for (var i = 0; i < args.length; i++) {
+ var converter = toC[argTypes[i]];
+ if (converter) {
+ if (stack === 0) stack = stackSave();
+ cArgs[i] = converter(args[i]);
+ } else {
+ cArgs[i] = args[i];
+ }
+ }
+ }
+ var ret = func.apply(null, cArgs);
+ function onDone(ret) {
+ if (stack !== 0) stackRestore(stack);
+ return convertReturnValue(ret);
+ }
+ ret = onDone(ret);
+ return ret;
+}
+
+function cwrap(ident, returnType, argTypes, opts) {
+ return function() {
+ return ccall(ident, returnType, argTypes, arguments, opts);
+ };
+}
+
+var ALLOC_NORMAL = 0;
+
+var ALLOC_STACK = 1;
+
+function allocate(slab, allocator) {
+ var ret;
+ assert(typeof allocator == "number", "allocate no longer takes a type argument");
+ assert(typeof slab != "number", "allocate no longer takes a number as arg0");
+ if (allocator == ALLOC_STACK) {
+ ret = stackAlloc(slab.length);
+ } else {
+ ret = _malloc(slab.length);
+ }
+ if (!slab.subarray && !slab.slice) {
+ slab = new Uint8Array(slab);
+ }
+ GROWABLE_HEAP_U8().set(slab, ret);
+ return ret;
+}
+
+function TextDecoderWrapper(encoding) {
+ var textDecoder = new TextDecoder(encoding);
+ this.decode = (data => {
+ assert(data instanceof Uint8Array);
+ if (data.buffer instanceof SharedArrayBuffer) {
+ data = new Uint8Array(data);
+ }
+ return textDecoder.decode.call(textDecoder, data);
+ });
+}
+
+var UTF8Decoder = typeof TextDecoder != "undefined" ? new TextDecoderWrapper("utf8") : undefined;
+
+function UTF8ArrayToString(heap, idx, maxBytesToRead) {
+ var endIdx = idx + maxBytesToRead;
+ var endPtr = idx;
+ while (heap[endPtr] && !(endPtr >= endIdx)) ++endPtr;
+ if (endPtr - idx > 16 && heap.subarray && UTF8Decoder) {
+ return UTF8Decoder.decode(heap.subarray(idx, endPtr));
+ } else {
+ var str = "";
+ while (idx < endPtr) {
+ var u0 = heap[idx++];
+ if (!(u0 & 128)) {
+ str += String.fromCharCode(u0);
+ continue;
+ }
+ var u1 = heap[idx++] & 63;
+ if ((u0 & 224) == 192) {
+ str += String.fromCharCode((u0 & 31) << 6 | u1);
+ continue;
+ }
+ var u2 = heap[idx++] & 63;
+ if ((u0 & 240) == 224) {
+ u0 = (u0 & 15) << 12 | u1 << 6 | u2;
+ } else {
+ if ((u0 & 248) != 240) warnOnce("Invalid UTF-8 leading byte 0x" + u0.toString(16) + " encountered when deserializing a UTF-8 string in wasm memory to a JS string!");
+ u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | heap[idx++] & 63;
+ }
+ if (u0 < 65536) {
+ str += String.fromCharCode(u0);
+ } else {
+ var ch = u0 - 65536;
+ str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023);
+ }
+ }
+ }
+ return str;
+}
+
+function UTF8ToString(ptr, maxBytesToRead) {
+ return ptr ? UTF8ArrayToString(GROWABLE_HEAP_U8(), ptr, maxBytesToRead) : "";
+}
+
+function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) {
+ if (!(maxBytesToWrite > 0)) return 0;
+ var startIdx = outIdx;
+ var endIdx = outIdx + maxBytesToWrite - 1;
+ for (var i = 0; i < str.length; ++i) {
+ var u = str.charCodeAt(i);
+ if (u >= 55296 && u <= 57343) {
+ var u1 = str.charCodeAt(++i);
+ u = 65536 + ((u & 1023) << 10) | u1 & 1023;
+ }
+ if (u <= 127) {
+ if (outIdx >= endIdx) break;
+ heap[outIdx++] = u;
+ } else if (u <= 2047) {
+ if (outIdx + 1 >= endIdx) break;
+ heap[outIdx++] = 192 | u >> 6;
+ heap[outIdx++] = 128 | u & 63;
+ } else if (u <= 65535) {
+ if (outIdx + 2 >= endIdx) break;
+ heap[outIdx++] = 224 | u >> 12;
+ heap[outIdx++] = 128 | u >> 6 & 63;
+ heap[outIdx++] = 128 | u & 63;
+ } else {
+ if (outIdx + 3 >= endIdx) break;
+ if (u > 1114111) warnOnce("Invalid Unicode code point 0x" + u.toString(16) + " encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).");
+ heap[outIdx++] = 240 | u >> 18;
+ heap[outIdx++] = 128 | u >> 12 & 63;
+ heap[outIdx++] = 128 | u >> 6 & 63;
+ heap[outIdx++] = 128 | u & 63;
+ }
+ }
+ heap[outIdx] = 0;
+ return outIdx - startIdx;
+}
+
+function stringToUTF8(str, outPtr, maxBytesToWrite) {
+ assert(typeof maxBytesToWrite == "number", "stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!");
+ return stringToUTF8Array(str, GROWABLE_HEAP_U8(), outPtr, maxBytesToWrite);
+}
+
+function lengthBytesUTF8(str) {
+ var len = 0;
+ for (var i = 0; i < str.length; ++i) {
+ var u = str.charCodeAt(i);
+ if (u >= 55296 && u <= 57343) u = 65536 + ((u & 1023) << 10) | str.charCodeAt(++i) & 1023;
+ if (u <= 127) ++len; else if (u <= 2047) len += 2; else if (u <= 65535) len += 3; else len += 4;
+ }
+ return len;
+}
+
+function AsciiToString(ptr) {
+ var str = "";
+ while (1) {
+ var ch = GROWABLE_HEAP_U8()[ptr++ >> 0];
+ if (!ch) return str;
+ str += String.fromCharCode(ch);
+ }
+}
+
+function stringToAscii(str, outPtr) {
+ return writeAsciiToMemory(str, outPtr, false);
+}
+
+var UTF16Decoder = typeof TextDecoder != "undefined" ? new TextDecoderWrapper("utf-16le") : undefined;
+
+function UTF16ToString(ptr, maxBytesToRead) {
+ assert(ptr % 2 == 0, "Pointer passed to UTF16ToString must be aligned to two bytes!");
+ var endPtr = ptr;
+ var idx = endPtr >> 1;
+ var maxIdx = idx + maxBytesToRead / 2;
+ while (!(idx >= maxIdx) && GROWABLE_HEAP_U16()[idx]) ++idx;
+ endPtr = idx << 1;
+ if (endPtr - ptr > 32 && UTF16Decoder) {
+ return UTF16Decoder.decode(GROWABLE_HEAP_U8().subarray(ptr, endPtr));
+ } else {
+ var str = "";
+ for (var i = 0; !(i >= maxBytesToRead / 2); ++i) {
+ var codeUnit = GROWABLE_HEAP_I16()[ptr + i * 2 >> 1];
+ if (codeUnit == 0) break;
+ str += String.fromCharCode(codeUnit);
+ }
+ return str;
+ }
+}
+
+function stringToUTF16(str, outPtr, maxBytesToWrite) {
+ assert(outPtr % 2 == 0, "Pointer passed to stringToUTF16 must be aligned to two bytes!");
+ assert(typeof maxBytesToWrite == "number", "stringToUTF16(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!");
+ if (maxBytesToWrite === undefined) {
+ maxBytesToWrite = 2147483647;
+ }
+ if (maxBytesToWrite < 2) return 0;
+ maxBytesToWrite -= 2;
+ var startPtr = outPtr;
+ var numCharsToWrite = maxBytesToWrite < str.length * 2 ? maxBytesToWrite / 2 : str.length;
+ for (var i = 0; i < numCharsToWrite; ++i) {
+ var codeUnit = str.charCodeAt(i);
+ GROWABLE_HEAP_I16()[outPtr >> 1] = codeUnit;
+ outPtr += 2;
+ }
+ GROWABLE_HEAP_I16()[outPtr >> 1] = 0;
+ return outPtr - startPtr;
+}
+
+function lengthBytesUTF16(str) {
+ return str.length * 2;
+}
+
+function UTF32ToString(ptr, maxBytesToRead) {
+ assert(ptr % 4 == 0, "Pointer passed to UTF32ToString must be aligned to four bytes!");
+ var i = 0;
+ var str = "";
+ while (!(i >= maxBytesToRead / 4)) {
+ var utf32 = GROWABLE_HEAP_I32()[ptr + i * 4 >> 2];
+ if (utf32 == 0) break;
+ ++i;
+ if (utf32 >= 65536) {
+ var ch = utf32 - 65536;
+ str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023);
+ } else {
+ str += String.fromCharCode(utf32);
+ }
+ }
+ return str;
+}
+
+function stringToUTF32(str, outPtr, maxBytesToWrite) {
+ assert(outPtr % 4 == 0, "Pointer passed to stringToUTF32 must be aligned to four bytes!");
+ assert(typeof maxBytesToWrite == "number", "stringToUTF32(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!");
+ if (maxBytesToWrite === undefined) {
+ maxBytesToWrite = 2147483647;
+ }
+ if (maxBytesToWrite < 4) return 0;
+ var startPtr = outPtr;
+ var endPtr = startPtr + maxBytesToWrite - 4;
+ for (var i = 0; i < str.length; ++i) {
+ var codeUnit = str.charCodeAt(i);
+ if (codeUnit >= 55296 && codeUnit <= 57343) {
+ var trailSurrogate = str.charCodeAt(++i);
+ codeUnit = 65536 + ((codeUnit & 1023) << 10) | trailSurrogate & 1023;
+ }
+ GROWABLE_HEAP_I32()[outPtr >> 2] = codeUnit;
+ outPtr += 4;
+ if (outPtr + 4 > endPtr) break;
+ }
+ GROWABLE_HEAP_I32()[outPtr >> 2] = 0;
+ return outPtr - startPtr;
+}
+
+function lengthBytesUTF32(str) {
+ var len = 0;
+ for (var i = 0; i < str.length; ++i) {
+ var codeUnit = str.charCodeAt(i);
+ if (codeUnit >= 55296 && codeUnit <= 57343) ++i;
+ len += 4;
+ }
+ return len;
+}
+
+function allocateUTF8(str) {
+ var size = lengthBytesUTF8(str) + 1;
+ var ret = _malloc(size);
+ if (ret) stringToUTF8Array(str, GROWABLE_HEAP_I8(), ret, size);
+ return ret;
+}
+
+function allocateUTF8OnStack(str) {
+ var size = lengthBytesUTF8(str) + 1;
+ var ret = stackAlloc(size);
+ stringToUTF8Array(str, GROWABLE_HEAP_I8(), ret, size);
+ return ret;
+}
+
+function writeStringToMemory(string, buffer, dontAddNull) {
+ warnOnce("writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!");
+ var lastChar, end;
+ if (dontAddNull) {
+ end = buffer + lengthBytesUTF8(string);
+ lastChar = GROWABLE_HEAP_I8()[end];
+ }
+ stringToUTF8(string, buffer, Infinity);
+ if (dontAddNull) GROWABLE_HEAP_I8()[end] = lastChar;
+}
+
+function writeArrayToMemory(array, buffer) {
+ assert(array.length >= 0, "writeArrayToMemory array must have a length (should be an array or typed array)");
+ GROWABLE_HEAP_I8().set(array, buffer);
+}
+
+function writeAsciiToMemory(str, buffer, dontAddNull) {
+ for (var i = 0; i < str.length; ++i) {
+ assert(str.charCodeAt(i) === (str.charCodeAt(i) & 255));
+ GROWABLE_HEAP_I8()[buffer++ >> 0] = str.charCodeAt(i);
+ }
+ if (!dontAddNull) GROWABLE_HEAP_I8()[buffer >> 0] = 0;
+}
+
+function alignUp(x, multiple) {
+ if (x % multiple > 0) {
+ x += multiple - x % multiple;
+ }
+ return x;
+}
+
+var HEAP, buffer, HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64;
+
+if (ENVIRONMENT_IS_PTHREAD) {
+ buffer = Module["buffer"];
+}
+
+function updateGlobalBufferAndViews(buf) {
+ buffer = buf;
+ Module["HEAP8"] = HEAP8 = new Int8Array(buf);
+ Module["HEAP16"] = HEAP16 = new Int16Array(buf);
+ Module["HEAP32"] = HEAP32 = new Int32Array(buf);
+ Module["HEAPU8"] = HEAPU8 = new Uint8Array(buf);
+ Module["HEAPU16"] = HEAPU16 = new Uint16Array(buf);
+ Module["HEAPU32"] = HEAPU32 = new Uint32Array(buf);
+ Module["HEAPF32"] = HEAPF32 = new Float32Array(buf);
+ Module["HEAPF64"] = HEAPF64 = new Float64Array(buf);
+}
+
+var TOTAL_STACK = 5242880;
+
+if (Module["TOTAL_STACK"]) assert(TOTAL_STACK === Module["TOTAL_STACK"], "the stack size can no longer be determined at runtime");
+
+var INITIAL_MEMORY = Module["INITIAL_MEMORY"] || 33554432;
+
+if (!Object.getOwnPropertyDescriptor(Module, "INITIAL_MEMORY")) {
+ Object.defineProperty(Module, "INITIAL_MEMORY", {
+ configurable: true,
+ get: function() {
+ abort("Module.INITIAL_MEMORY has been replaced with plain INITIAL_MEMORY (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)");
+ }
+ });
+}
+
+assert(INITIAL_MEMORY >= TOTAL_STACK, "INITIAL_MEMORY should be larger than TOTAL_STACK, was " + INITIAL_MEMORY + "! (TOTAL_STACK=" + TOTAL_STACK + ")");
+
+assert(typeof Int32Array != "undefined" && typeof Float64Array !== "undefined" && Int32Array.prototype.subarray != undefined && Int32Array.prototype.set != undefined, "JS engine does not provide full typed array support");
+
+if (ENVIRONMENT_IS_PTHREAD) {
+ wasmMemory = Module["wasmMemory"];
+ buffer = Module["buffer"];
+} else {
+ if (Module["wasmMemory"]) {
+ wasmMemory = Module["wasmMemory"];
+ } else {
+ wasmMemory = new WebAssembly.Memory({
+ "initial": INITIAL_MEMORY / 65536,
+ "maximum": 2147483648 / 65536,
+ "shared": true
+ });
+ if (!(wasmMemory.buffer instanceof SharedArrayBuffer)) {
+ err("requested a shared WebAssembly.Memory but the returned buffer is not a SharedArrayBuffer, indicating that while the browser has SharedArrayBuffer it does not have WebAssembly threads support - you may need to set a flag");
+ if (ENVIRONMENT_IS_NODE) {
+ console.log("(on node you may need: --experimental-wasm-threads --experimental-wasm-bulk-memory and also use a recent version)");
+ }
+ throw Error("bad memory");
+ }
+ }
+}
+
+if (wasmMemory) {
+ buffer = wasmMemory.buffer;
+}
+
+INITIAL_MEMORY = buffer.byteLength;
+
+assert(INITIAL_MEMORY % 65536 === 0);
+
+updateGlobalBufferAndViews(buffer);
+
+var wasmTable;
+
+function writeStackCookie() {
+ var max = _emscripten_stack_get_end();
+ assert((max & 3) == 0);
+ GROWABLE_HEAP_I32()[max + 4 >> 2] = 34821223;
+ GROWABLE_HEAP_I32()[max + 8 >> 2] = 2310721022;
+ GROWABLE_HEAP_I32()[0] = 1668509029;
+}
+
+function checkStackCookie() {
+ if (ABORT) return;
+ var max = _emscripten_stack_get_end();
+ var cookie1 = GROWABLE_HEAP_U32()[max + 4 >> 2];
+ var cookie2 = GROWABLE_HEAP_U32()[max + 8 >> 2];
+ if (cookie1 != 34821223 || cookie2 != 2310721022) {
+ abort("Stack overflow! Stack cookie has been overwritten, expected hex dwords 0x89BACDFE and 0x2135467, but received 0x" + cookie2.toString(16) + " 0x" + cookie1.toString(16));
+ }
+ if (GROWABLE_HEAP_I32()[0] !== 1668509029) abort("Runtime error: The application has corrupted its heap memory area (address zero)!");
+}
+
+(function() {
+ var h16 = new Int16Array(1);
+ var h8 = new Int8Array(h16.buffer);
+ h16[0] = 25459;
+ if (h8[0] !== 115 || h8[1] !== 99) throw "Runtime error: expected the system to be little-endian! (Run with -s SUPPORT_BIG_ENDIAN=1 to bypass)";
+})();
+
+var __ATPRERUN__ = [];
+
+var __ATINIT__ = [];
+
+var __ATMAIN__ = [];
+
+var __ATEXIT__ = [];
+
+var __ATPOSTRUN__ = [];
+
+var runtimeInitialized = false;
+
+var runtimeExited = false;
+
+var runtimeKeepaliveCounter = 0;
+
+function keepRuntimeAlive() {
+ return noExitRuntime || runtimeKeepaliveCounter > 0;
+}
+
+function preRun() {
+ assert(!ENVIRONMENT_IS_PTHREAD);
+ if (Module["preRun"]) {
+ if (typeof Module["preRun"] == "function") Module["preRun"] = [ Module["preRun"] ];
+ while (Module["preRun"].length) {
+ addOnPreRun(Module["preRun"].shift());
+ }
+ }
+ callRuntimeCallbacks(__ATPRERUN__);
+}
+
+function initRuntime() {
+ checkStackCookie();
+ assert(!runtimeInitialized);
+ runtimeInitialized = true;
+ if (ENVIRONMENT_IS_PTHREAD) return;
+ if (!Module["noFSInit"] && !FS.init.initialized) FS.init();
+ FS.ignorePermissions = false;
+ TTY.init();
+ SOCKFS.root = FS.mount(SOCKFS, {}, null);
+ callRuntimeCallbacks(__ATINIT__);
+}
+
+function preMain() {
+ checkStackCookie();
+ if (ENVIRONMENT_IS_PTHREAD) return;
+ callRuntimeCallbacks(__ATMAIN__);
+}
+
+function exitRuntime() {
+ checkStackCookie();
+ if (ENVIRONMENT_IS_PTHREAD) return;
+ ___funcs_on_exit();
+ callRuntimeCallbacks(__ATEXIT__);
+ FS.quit();
+ TTY.shutdown();
+ PThread.terminateAllThreads();
+ runtimeExited = true;
+}
+
+function postRun() {
+ checkStackCookie();
+ if (ENVIRONMENT_IS_PTHREAD) return;
+ if (Module["postRun"]) {
+ if (typeof Module["postRun"] == "function") Module["postRun"] = [ Module["postRun"] ];
+ while (Module["postRun"].length) {
+ addOnPostRun(Module["postRun"].shift());
+ }
+ }
+ callRuntimeCallbacks(__ATPOSTRUN__);
+}
+
+function addOnPreRun(cb) {
+ __ATPRERUN__.unshift(cb);
+}
+
+function addOnInit(cb) {
+ __ATINIT__.unshift(cb);
+}
+
+function addOnPreMain(cb) {
+ __ATMAIN__.unshift(cb);
+}
+
+function addOnExit(cb) {
+ __ATEXIT__.unshift(cb);
+}
+
+function addOnPostRun(cb) {
+ __ATPOSTRUN__.unshift(cb);
+}
+
+assert(Math.imul, "This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");
+
+assert(Math.fround, "This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");
+
+assert(Math.clz32, "This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");
+
+assert(Math.trunc, "This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");
+
+var runDependencies = 0;
+
+var runDependencyWatcher = null;
+
+var dependenciesFulfilled = null;
+
+var runDependencyTracking = {};
+
+function getUniqueRunDependency(id) {
+ var orig = id;
+ while (1) {
+ if (!runDependencyTracking[id]) return id;
+ id = orig + Math.random();
+ }
+}
+
+function addRunDependency(id) {
+ runDependencies++;
+ if (Module["monitorRunDependencies"]) {
+ Module["monitorRunDependencies"](runDependencies);
+ }
+ if (id) {
+ assert(!runDependencyTracking[id]);
+ runDependencyTracking[id] = 1;
+ if (runDependencyWatcher === null && typeof setInterval != "undefined") {
+ runDependencyWatcher = setInterval(function() {
+ if (ABORT) {
+ clearInterval(runDependencyWatcher);
+ runDependencyWatcher = null;
+ return;
+ }
+ var shown = false;
+ for (var dep in runDependencyTracking) {
+ if (!shown) {
+ shown = true;
+ err("still waiting on run dependencies:");
+ }
+ err("dependency: " + dep);
+ }
+ if (shown) {
+ err("(end of list)");
+ }
+ }, 1e4);
+ }
+ } else {
+ err("warning: run dependency added without ID");
+ }
+}
+
+function removeRunDependency(id) {
+ runDependencies--;
+ if (Module["monitorRunDependencies"]) {
+ Module["monitorRunDependencies"](runDependencies);
+ }
+ if (id) {
+ assert(runDependencyTracking[id]);
+ delete runDependencyTracking[id];
+ } else {
+ err("warning: run dependency removed without ID");
+ }
+ if (runDependencies == 0) {
+ if (runDependencyWatcher !== null) {
+ clearInterval(runDependencyWatcher);
+ runDependencyWatcher = null;
+ }
+ if (dependenciesFulfilled) {
+ var callback = dependenciesFulfilled;
+ dependenciesFulfilled = null;
+ callback();
+ }
+ }
+}
+
+Module["preloadedImages"] = {};
+
+Module["preloadedAudios"] = {};
+
+function abort(what) {
+ if (ENVIRONMENT_IS_PTHREAD) {
+ postMessage({
+ "cmd": "onAbort",
+ "arg": what
+ });
+ } else {
+ if (Module["onAbort"]) {
+ Module["onAbort"](what);
+ }
+ }
+ what = "Aborted(" + what + ")";
+ err(what);
+ ABORT = true;
+ EXITSTATUS = 1;
+ var e = new WebAssembly.RuntimeError(what);
+ readyPromiseReject(e);
+ throw e;
+}
+
+var dataURIPrefix = "data:application/octet-stream;base64,";
+
+function isDataURI(filename) {
+ return filename.startsWith(dataURIPrefix);
+}
+
+function isFileURI(filename) {
+ return filename.startsWith("file://");
+}
+
+function createExportWrapper(name, fixedasm) {
+ return function() {
+ var displayName = name;
+ var asm = fixedasm;
+ if (!fixedasm) {
+ asm = Module["asm"];
+ }
+ assert(runtimeInitialized, "native function `" + displayName + "` called before runtime initialization");
+ assert(!runtimeExited, "native function `" + displayName + "` called after runtime exit (use NO_EXIT_RUNTIME to keep it alive after main() exits)");
+ if (!asm[name]) {
+ assert(asm[name], "exported native function `" + displayName + "` not found");
+ }
+ return asm[name].apply(null, arguments);
+ };
+}
+
+var wasmBinaryFile;
+
+wasmBinaryFile = "godot.web.template_release.wasm32.wasm";
+
+if (!isDataURI(wasmBinaryFile)) {
+ wasmBinaryFile = locateFile(wasmBinaryFile);
+}
+
+function getBinary(file) {
+ try {
+ if (file == wasmBinaryFile && wasmBinary) {
+ return new Uint8Array(wasmBinary);
+ }
+ if (readBinary) {
+ return readBinary(file);
+ } else {
+ throw "both async and sync fetching of the wasm failed";
+ }
+ } catch (err) {
+ abort(err);
+ }
+}
+
+function getBinaryPromise() {
+ if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) {
+ if (typeof fetch == "function") {
+ return fetch(wasmBinaryFile, {
+ credentials: "same-origin"
+ }).then(function(response) {
+ if (!response["ok"]) {
+ throw "failed to load wasm binary file at '" + wasmBinaryFile + "'";
+ }
+ return response["arrayBuffer"]();
+ }).catch(function() {
+ return getBinary(wasmBinaryFile);
+ });
+ }
+ }
+ return Promise.resolve().then(function() {
+ return getBinary(wasmBinaryFile);
+ });
+}
+
+function createWasm() {
+ var info = {
+ "env": asmLibraryArg,
+ "wasi_snapshot_preview1": asmLibraryArg
+ };
+ function receiveInstance(instance, module) {
+ var exports = instance.exports;
+ Module["asm"] = exports;
+ registerTlsInit(Module["asm"]["emscripten_tls_init"]);
+ wasmTable = Module["asm"]["__indirect_function_table"];
+ assert(wasmTable, "table not found in wasm exports");
+ addOnInit(Module["asm"]["__wasm_call_ctors"]);
+ wasmModule = module;
+ if (!ENVIRONMENT_IS_PTHREAD) {
+ var numWorkersToLoad = PThread.unusedWorkers.length;
+ PThread.unusedWorkers.forEach(function(w) {
+ PThread.loadWasmModuleToWorker(w, function() {
+ if (!--numWorkersToLoad) removeRunDependency("wasm-instantiate");
+ });
+ });
+ }
+ }
+ if (!ENVIRONMENT_IS_PTHREAD) {
+ addRunDependency("wasm-instantiate");
+ }
+ var trueModule = Module;
+ function receiveInstantiationResult(result) {
+ assert(Module === trueModule, "the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?");
+ trueModule = null;
+ receiveInstance(result["instance"], result["module"]);
+ }
+ function instantiateArrayBuffer(receiver) {
+ return getBinaryPromise().then(function(binary) {
+ return WebAssembly.instantiate(binary, info);
+ }).then(function(instance) {
+ return instance;
+ }).then(receiver, function(reason) {
+ err("failed to asynchronously prepare wasm: " + reason);
+ if (isFileURI(wasmBinaryFile)) {
+ err("warning: Loading from a file URI (" + wasmBinaryFile + ") is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing");
+ }
+ abort(reason);
+ });
+ }
+ function instantiateAsync() {
+ if (!wasmBinary && typeof WebAssembly.instantiateStreaming == "function" && !isDataURI(wasmBinaryFile) && typeof fetch == "function") {
+ return fetch(wasmBinaryFile, {
+ credentials: "same-origin"
+ }).then(function(response) {
+ var result = WebAssembly.instantiateStreaming(response, info);
+ return result.then(receiveInstantiationResult, function(reason) {
+ err("wasm streaming compile failed: " + reason);
+ err("falling back to ArrayBuffer instantiation");
+ return instantiateArrayBuffer(receiveInstantiationResult);
+ });
+ });
+ } else {
+ return instantiateArrayBuffer(receiveInstantiationResult);
+ }
+ }
+ if (Module["instantiateWasm"]) {
+ try {
+ var exports = Module["instantiateWasm"](info, receiveInstance);
+ return exports;
+ } catch (e) {
+ err("Module.instantiateWasm callback failed with error: " + e);
+ return false;
+ }
+ }
+ instantiateAsync().catch(readyPromiseReject);
+ return {};
+}
+
+var tempDouble;
+
+var tempI64;
+
+var ASM_CONSTS = {};
+
+function callRuntimeCallbacks(callbacks) {
+ while (callbacks.length > 0) {
+ var callback = callbacks.shift();
+ if (typeof callback == "function") {
+ callback(Module);
+ continue;
+ }
+ var func = callback.func;
+ if (typeof func == "number") {
+ if (callback.arg === undefined) {
+ getWasmTableEntry(func)();
+ } else {
+ getWasmTableEntry(func)(callback.arg);
+ }
+ } else {
+ func(callback.arg === undefined ? null : callback.arg);
+ }
+ }
+}
+
+function withStackSave(f) {
+ var stack = stackSave();
+ var ret = f();
+ stackRestore(stack);
+ return ret;
+}
+
+function demangle(func) {
+ warnOnce("warning: build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling");
+ return func;
+}
+
+function demangleAll(text) {
+ var regex = /\b_Z[\w\d_]+/g;
+ return text.replace(regex, function(x) {
+ var y = demangle(x);
+ return x === y ? x : y + " [" + x + "]";
+ });
+}
+
+function killThread(pthread_ptr) {
+ assert(!ENVIRONMENT_IS_PTHREAD, "Internal Error! killThread() can only ever be called from main application thread!");
+ assert(pthread_ptr, "Internal Error! Null pthread_ptr in killThread!");
+ GROWABLE_HEAP_I32()[pthread_ptr >> 2] = 0;
+ var pthread = PThread.pthreads[pthread_ptr];
+ delete PThread.pthreads[pthread_ptr];
+ pthread.worker.terminate();
+ __emscripten_thread_free_data(pthread_ptr);
+ PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(pthread.worker), 1);
+ pthread.worker.pthread = undefined;
+}
+
+function cancelThread(pthread_ptr) {
+ assert(!ENVIRONMENT_IS_PTHREAD, "Internal Error! cancelThread() can only ever be called from main application thread!");
+ assert(pthread_ptr, "Internal Error! Null pthread_ptr in cancelThread!");
+ var pthread = PThread.pthreads[pthread_ptr];
+ pthread.worker.postMessage({
+ "cmd": "cancel"
+ });
+}
+
+function cleanupThread(pthread_ptr) {
+ assert(!ENVIRONMENT_IS_PTHREAD, "Internal Error! cleanupThread() can only ever be called from main application thread!");
+ assert(pthread_ptr, "Internal Error! Null pthread_ptr in cleanupThread!");
+ var pthread = PThread.pthreads[pthread_ptr];
+ if (pthread) {
+ GROWABLE_HEAP_I32()[pthread_ptr >> 2] = 0;
+ var worker = pthread.worker;
+ PThread.returnWorkerToPool(worker);
+ }
+}
+
+function zeroMemory(address, size) {
+ GROWABLE_HEAP_U8().fill(0, address, address + size);
+}
+
+function ptrToString(ptr) {
+ return "0x" + ptr.toString(16).padStart(8, "0");
+}
+
+function _exit(status) {
+ exit(status);
+}
+
+function handleException(e) {
+ if (e instanceof ExitStatus || e == "unwind") {
+ return EXITSTATUS;
+ }
+ quit_(1, e);
+}
+
+var PThread = {
+ unusedWorkers: [],
+ runningWorkers: [],
+ tlsInitFunctions: [],
+ init: function() {
+ if (ENVIRONMENT_IS_PTHREAD) {
+ PThread.initWorker();
+ } else {
+ PThread.initMainThread();
+ }
+ },
+ initMainThread: function() {
+ var pthreadPoolSize = 8;
+ for (var i = 0; i < pthreadPoolSize; ++i) {
+ PThread.allocateUnusedWorker();
+ }
+ },
+ initWorker: function() {
+ noExitRuntime = false;
+ },
+ pthreads: {},
+ setExitStatus: function(status) {
+ EXITSTATUS = status;
+ },
+ terminateAllThreads: function() {
+ assert(!ENVIRONMENT_IS_PTHREAD, "Internal Error! terminateAllThreads() can only ever be called from main application thread!");
+ for (var t in PThread.pthreads) {
+ var pthread = PThread.pthreads[t];
+ if (pthread && pthread.worker) {
+ PThread.returnWorkerToPool(pthread.worker);
+ }
+ }
+ assert(Object.keys(PThread.pthreads).length === 0);
+ assert(PThread.runningWorkers.length === 0);
+ for (var i = 0; i < PThread.unusedWorkers.length; ++i) {
+ var worker = PThread.unusedWorkers[i];
+ assert(!worker.pthread);
+ worker.terminate();
+ }
+ PThread.unusedWorkers = [];
+ },
+ returnWorkerToPool: function(worker) {
+ PThread.runWithoutMainThreadQueuedCalls(function() {
+ delete PThread.pthreads[worker.pthread.threadInfoStruct];
+ PThread.unusedWorkers.push(worker);
+ PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker), 1);
+ __emscripten_thread_free_data(worker.pthread.threadInfoStruct);
+ worker.pthread = undefined;
+ });
+ },
+ runWithoutMainThreadQueuedCalls: function(func) {
+ assert(PThread.mainRuntimeThread, "runWithoutMainThreadQueuedCalls must be done on the main runtime thread");
+ assert(__emscripten_allow_main_runtime_queued_calls);
+ GROWABLE_HEAP_I32()[__emscripten_allow_main_runtime_queued_calls >> 2] = 0;
+ try {
+ func();
+ } finally {
+ GROWABLE_HEAP_I32()[__emscripten_allow_main_runtime_queued_calls >> 2] = 1;
+ }
+ },
+ receiveObjectTransfer: function(data) {},
+ threadInit: function() {
+ for (var i in PThread.tlsInitFunctions) {
+ PThread.tlsInitFunctions[i]();
+ }
+ },
+ loadWasmModuleToWorker: function(worker, onFinishedLoading) {
+ worker.onmessage = (e => {
+ var d = e["data"];
+ var cmd = d["cmd"];
+ if (worker.pthread) PThread.currentProxiedOperationCallerThread = worker.pthread.threadInfoStruct;
+ if (d["targetThread"] && d["targetThread"] != _pthread_self()) {
+ var thread = PThread.pthreads[d.targetThread];
+ if (thread) {
+ thread.worker.postMessage(d, d["transferList"]);
+ } else {
+ err('Internal error! Worker sent a message "' + cmd + '" to target pthread ' + d["targetThread"] + ", but that thread no longer exists!");
+ }
+ PThread.currentProxiedOperationCallerThread = undefined;
+ return;
+ }
+ if (cmd === "processQueuedMainThreadWork") {
+ _emscripten_main_thread_process_queued_calls();
+ } else if (cmd === "spawnThread") {
+ spawnThread(d);
+ } else if (cmd === "cleanupThread") {
+ cleanupThread(d["thread"]);
+ } else if (cmd === "killThread") {
+ killThread(d["thread"]);
+ } else if (cmd === "cancelThread") {
+ cancelThread(d["thread"]);
+ } else if (cmd === "loaded") {
+ worker.loaded = true;
+ if (onFinishedLoading) onFinishedLoading(worker);
+ if (worker.runPthread) {
+ worker.runPthread();
+ delete worker.runPthread;
+ }
+ } else if (cmd === "print") {
+ out("Thread " + d["threadId"] + ": " + d["text"]);
+ } else if (cmd === "printErr") {
+ err("Thread " + d["threadId"] + ": " + d["text"]);
+ } else if (cmd === "alert") {
+ alert("Thread " + d["threadId"] + ": " + d["text"]);
+ } else if (d.target === "setimmediate") {
+ worker.postMessage(d);
+ } else if (cmd === "onAbort") {
+ if (Module["onAbort"]) {
+ Module["onAbort"](d["arg"]);
+ }
+ } else {
+ err("worker sent an unknown command " + cmd);
+ }
+ PThread.currentProxiedOperationCallerThread = undefined;
+ });
+ worker.onerror = (e => {
+ var message = "worker sent an error!";
+ if (worker.pthread) {
+ var pthread_ptr = worker.pthread.threadInfoStruct;
+ if (pthread_ptr) {
+ message = "Pthread " + ptrToString(pthread_ptr) + " sent an error!";
+ }
+ }
+ err(message + " " + e.filename + ":" + e.lineno + ": " + e.message);
+ throw e;
+ });
+ assert(wasmMemory instanceof WebAssembly.Memory, "WebAssembly memory should have been loaded by now!");
+ assert(wasmModule instanceof WebAssembly.Module, "WebAssembly Module should have been loaded by now!");
+ worker.postMessage({
+ "cmd": "load",
+ "urlOrBlob": Module["mainScriptUrlOrBlob"] || _scriptDir,
+ "wasmMemory": wasmMemory,
+ "wasmModule": wasmModule
+ });
+ },
+ allocateUnusedWorker: function() {
+ var pthreadMainJs = locateFile("godot.web.template_release.wasm32.worker.js");
+ PThread.unusedWorkers.push(new Worker(pthreadMainJs));
+ },
+ getNewWorker: function() {
+ if (PThread.unusedWorkers.length == 0) {
+ err("Tried to spawn a new thread, but the thread pool is exhausted.\n" + "This might result in a deadlock unless some threads eventually exit or the code explicitly breaks out to the event loop.\n" + "If you want to increase the pool size, use setting `-s PTHREAD_POOL_SIZE=...`." + "\nIf you want to throw an explicit error instead of the risk of deadlocking in those cases, use setting `-s PTHREAD_POOL_SIZE_STRICT=2`.");
+ PThread.allocateUnusedWorker();
+ PThread.loadWasmModuleToWorker(PThread.unusedWorkers[0]);
+ }
+ return PThread.unusedWorkers.pop();
+ }
+};
+
+function establishStackSpace() {
+ var pthread_ptr = _pthread_self();
+ var stackTop = GROWABLE_HEAP_I32()[pthread_ptr + 44 >> 2];
+ var stackSize = GROWABLE_HEAP_I32()[pthread_ptr + 48 >> 2];
+ var stackMax = stackTop - stackSize;
+ assert(stackTop != 0);
+ assert(stackMax != 0);
+ assert(stackTop > stackMax);
+ _emscripten_stack_set_limits(stackTop, stackMax);
+ stackRestore(stackTop);
+ writeStackCookie();
+}
+
+Module["establishStackSpace"] = establishStackSpace;
+
+function exitOnMainThread(returnCode) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(1, 0, returnCode);
+ try {
+ _exit(returnCode);
+ } catch (e) {
+ handleException(e);
+ }
+}
+
+var wasmTableMirror = [];
+
+function getWasmTableEntry(funcPtr) {
+ var func = wasmTableMirror[funcPtr];
+ if (!func) {
+ if (funcPtr >= wasmTableMirror.length) wasmTableMirror.length = funcPtr + 1;
+ wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr);
+ }
+ assert(wasmTable.get(funcPtr) == func, "JavaScript-side Wasm function table mirror is out of date!");
+ return func;
+}
+
+function invokeEntryPoint(ptr, arg) {
+ return getWasmTableEntry(ptr)(arg);
+}
+
+Module["invokeEntryPoint"] = invokeEntryPoint;
+
+function jsStackTrace() {
+ var error = new Error();
+ if (!error.stack) {
+ try {
+ throw new Error();
+ } catch (e) {
+ error = e;
+ }
+ if (!error.stack) {
+ return "(no stack trace available)";
+ }
+ }
+ return error.stack.toString();
+}
+
+function registerTlsInit(tlsInitFunc) {
+ PThread.tlsInitFunctions.push(tlsInitFunc);
+}
+
+function setWasmTableEntry(idx, func) {
+ wasmTable.set(idx, func);
+ wasmTableMirror[idx] = func;
+}
+
+function stackTrace() {
+ var js = jsStackTrace();
+ if (Module["extraStackTrace"]) js += "\n" + Module["extraStackTrace"]();
+ return demangleAll(js);
+}
+
+function ___assert_fail(condition, filename, line, func) {
+ abort("Assertion failed: " + UTF8ToString(condition) + ", at: " + [ filename ? UTF8ToString(filename) : "unknown filename", line, func ? UTF8ToString(func) : "unknown function" ]);
+}
+
+function ___call_sighandler(fp, sig) {
+ getWasmTableEntry(fp)(sig);
+}
+
+var _emscripten_get_now;
+
+if (ENVIRONMENT_IS_PTHREAD) {
+ _emscripten_get_now = (() => performance.now() - Module["__performance_now_clock_drift"]);
+} else _emscripten_get_now = (() => performance.now());
+
+var _emscripten_get_now_is_monotonic = true;
+
+function setErrNo(value) {
+ GROWABLE_HEAP_I32()[___errno_location() >> 2] = value;
+ return value;
+}
+
+function _clock_gettime(clk_id, tp) {
+ var now;
+ if (clk_id === 0) {
+ now = Date.now();
+ } else if ((clk_id === 1 || clk_id === 4) && _emscripten_get_now_is_monotonic) {
+ now = _emscripten_get_now();
+ } else {
+ setErrNo(28);
+ return -1;
+ }
+ GROWABLE_HEAP_I32()[tp >> 2] = now / 1e3 | 0;
+ GROWABLE_HEAP_I32()[tp + 4 >> 2] = now % 1e3 * 1e3 * 1e3 | 0;
+ return 0;
+}
+
+function ___clock_gettime(a0, a1) {
+ return _clock_gettime(a0, a1);
+}
+
+function ___emscripten_init_main_thread_js(tb) {
+ __emscripten_thread_init(tb, !ENVIRONMENT_IS_WORKER, 1, !ENVIRONMENT_IS_WEB);
+ PThread.mainRuntimeThread = true;
+ PThread.threadInit();
+}
+
+function ___emscripten_thread_cleanup(thread) {
+ if (!ENVIRONMENT_IS_PTHREAD) cleanupThread(thread); else postMessage({
+ "cmd": "cleanupThread",
+ "thread": thread
+ });
+}
+
+function spawnThread(threadParams) {
+ assert(!ENVIRONMENT_IS_PTHREAD, "Internal Error! spawnThread() can only ever be called from main application thread!");
+ assert(threadParams.pthread_ptr, "Internal error, no pthread ptr!");
+ var worker = PThread.getNewWorker();
+ if (!worker) {
+ return 6;
+ }
+ assert(!worker.pthread, "Internal error!");
+ PThread.runningWorkers.push(worker);
+ var pthread = PThread.pthreads[threadParams.pthread_ptr] = {
+ worker: worker,
+ threadInfoStruct: threadParams.pthread_ptr
+ };
+ worker.pthread = pthread;
+ var msg = {
+ "cmd": "run",
+ "start_routine": threadParams.startRoutine,
+ "arg": threadParams.arg,
+ "threadInfoStruct": threadParams.pthread_ptr
+ };
+ worker.runPthread = (() => {
+ msg.time = performance.now();
+ worker.postMessage(msg, threadParams.transferList);
+ });
+ if (worker.loaded) {
+ worker.runPthread();
+ delete worker.runPthread;
+ }
+ return 0;
+}
+
+function ___pthread_create_js(pthread_ptr, attr, start_routine, arg) {
+ if (typeof SharedArrayBuffer == "undefined") {
+ err("Current environment does not support SharedArrayBuffer, pthreads are not available!");
+ return 6;
+ }
+ var transferList = [];
+ var error = 0;
+ if (ENVIRONMENT_IS_PTHREAD && (transferList.length === 0 || error)) {
+ return _emscripten_sync_run_in_main_thread_4(687865856, pthread_ptr, attr, start_routine, arg);
+ }
+ if (error) return error;
+ var threadParams = {
+ startRoutine: start_routine,
+ pthread_ptr: pthread_ptr,
+ arg: arg,
+ transferList: transferList
+ };
+ if (ENVIRONMENT_IS_PTHREAD) {
+ threadParams.cmd = "spawnThread";
+ postMessage(threadParams, transferList);
+ return 0;
+ }
+ return spawnThread(threadParams);
+}
+
+var PATH = {
+ splitPath: function(filename) {
+ var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
+ return splitPathRe.exec(filename).slice(1);
+ },
+ normalizeArray: function(parts, allowAboveRoot) {
+ var up = 0;
+ for (var i = parts.length - 1; i >= 0; i--) {
+ var last = parts[i];
+ if (last === ".") {
+ parts.splice(i, 1);
+ } else if (last === "..") {
+ parts.splice(i, 1);
+ up++;
+ } else if (up) {
+ parts.splice(i, 1);
+ up--;
+ }
+ }
+ if (allowAboveRoot) {
+ for (;up; up--) {
+ parts.unshift("..");
+ }
+ }
+ return parts;
+ },
+ normalize: function(path) {
+ var isAbsolute = path.charAt(0) === "/", trailingSlash = path.substr(-1) === "/";
+ path = PATH.normalizeArray(path.split("/").filter(function(p) {
+ return !!p;
+ }), !isAbsolute).join("/");
+ if (!path && !isAbsolute) {
+ path = ".";
+ }
+ if (path && trailingSlash) {
+ path += "/";
+ }
+ return (isAbsolute ? "/" : "") + path;
+ },
+ dirname: function(path) {
+ var result = PATH.splitPath(path), root = result[0], dir = result[1];
+ if (!root && !dir) {
+ return ".";
+ }
+ if (dir) {
+ dir = dir.substr(0, dir.length - 1);
+ }
+ return root + dir;
+ },
+ basename: function(path) {
+ if (path === "/") return "/";
+ path = PATH.normalize(path);
+ path = path.replace(/\/$/, "");
+ var lastSlash = path.lastIndexOf("/");
+ if (lastSlash === -1) return path;
+ return path.substr(lastSlash + 1);
+ },
+ extname: function(path) {
+ return PATH.splitPath(path)[3];
+ },
+ join: function() {
+ var paths = Array.prototype.slice.call(arguments, 0);
+ return PATH.normalize(paths.join("/"));
+ },
+ join2: function(l, r) {
+ return PATH.normalize(l + "/" + r);
+ }
+};
+
+function getRandomDevice() {
+ if (typeof crypto == "object" && typeof crypto["getRandomValues"] == "function") {
+ var randomBuffer = new Uint8Array(1);
+ return function() {
+ crypto.getRandomValues(randomBuffer);
+ return randomBuffer[0];
+ };
+ } else return function() {
+ abort("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: function(array) { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };");
+ };
+}
+
+var PATH_FS = {
+ resolve: function() {
+ var resolvedPath = "", resolvedAbsolute = false;
+ for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
+ var path = i >= 0 ? arguments[i] : FS.cwd();
+ if (typeof path != "string") {
+ throw new TypeError("Arguments to path.resolve must be strings");
+ } else if (!path) {
+ return "";
+ }
+ resolvedPath = path + "/" + resolvedPath;
+ resolvedAbsolute = path.charAt(0) === "/";
+ }
+ resolvedPath = PATH.normalizeArray(resolvedPath.split("/").filter(function(p) {
+ return !!p;
+ }), !resolvedAbsolute).join("/");
+ return (resolvedAbsolute ? "/" : "") + resolvedPath || ".";
+ },
+ relative: function(from, to) {
+ from = PATH_FS.resolve(from).substr(1);
+ to = PATH_FS.resolve(to).substr(1);
+ function trim(arr) {
+ var start = 0;
+ for (;start < arr.length; start++) {
+ if (arr[start] !== "") break;
+ }
+ var end = arr.length - 1;
+ for (;end >= 0; end--) {
+ if (arr[end] !== "") break;
+ }
+ if (start > end) return [];
+ return arr.slice(start, end - start + 1);
+ }
+ var fromParts = trim(from.split("/"));
+ var toParts = trim(to.split("/"));
+ var length = Math.min(fromParts.length, toParts.length);
+ var samePartsLength = length;
+ for (var i = 0; i < length; i++) {
+ if (fromParts[i] !== toParts[i]) {
+ samePartsLength = i;
+ break;
+ }
+ }
+ var outputParts = [];
+ for (var i = samePartsLength; i < fromParts.length; i++) {
+ outputParts.push("..");
+ }
+ outputParts = outputParts.concat(toParts.slice(samePartsLength));
+ return outputParts.join("/");
+ }
+};
+
+var TTY = {
+ ttys: [],
+ init: function() {},
+ shutdown: function() {},
+ register: function(dev, ops) {
+ TTY.ttys[dev] = {
+ input: [],
+ output: [],
+ ops: ops
+ };
+ FS.registerDevice(dev, TTY.stream_ops);
+ },
+ stream_ops: {
+ open: function(stream) {
+ var tty = TTY.ttys[stream.node.rdev];
+ if (!tty) {
+ throw new FS.ErrnoError(43);
+ }
+ stream.tty = tty;
+ stream.seekable = false;
+ },
+ close: function(stream) {
+ stream.tty.ops.flush(stream.tty);
+ },
+ flush: function(stream) {
+ stream.tty.ops.flush(stream.tty);
+ },
+ read: function(stream, buffer, offset, length, pos) {
+ if (!stream.tty || !stream.tty.ops.get_char) {
+ throw new FS.ErrnoError(60);
+ }
+ var bytesRead = 0;
+ for (var i = 0; i < length; i++) {
+ var result;
+ try {
+ result = stream.tty.ops.get_char(stream.tty);
+ } catch (e) {
+ throw new FS.ErrnoError(29);
+ }
+ if (result === undefined && bytesRead === 0) {
+ throw new FS.ErrnoError(6);
+ }
+ if (result === null || result === undefined) break;
+ bytesRead++;
+ buffer[offset + i] = result;
+ }
+ if (bytesRead) {
+ stream.node.timestamp = Date.now();
+ }
+ return bytesRead;
+ },
+ write: function(stream, buffer, offset, length, pos) {
+ if (!stream.tty || !stream.tty.ops.put_char) {
+ throw new FS.ErrnoError(60);
+ }
+ try {
+ for (var i = 0; i < length; i++) {
+ stream.tty.ops.put_char(stream.tty, buffer[offset + i]);
+ }
+ } catch (e) {
+ throw new FS.ErrnoError(29);
+ }
+ if (length) {
+ stream.node.timestamp = Date.now();
+ }
+ return i;
+ }
+ },
+ default_tty_ops: {
+ get_char: function(tty) {
+ if (!tty.input.length) {
+ var result = null;
+ if (typeof window != "undefined" && typeof window.prompt == "function") {
+ result = window.prompt("Input: ");
+ if (result !== null) {
+ result += "\n";
+ }
+ } else if (typeof readline == "function") {
+ result = readline();
+ if (result !== null) {
+ result += "\n";
+ }
+ }
+ if (!result) {
+ return null;
+ }
+ tty.input = intArrayFromString(result, true);
+ }
+ return tty.input.shift();
+ },
+ put_char: function(tty, val) {
+ if (val === null || val === 10) {
+ out(UTF8ArrayToString(tty.output, 0));
+ tty.output = [];
+ } else {
+ if (val != 0) tty.output.push(val);
+ }
+ },
+ flush: function(tty) {
+ if (tty.output && tty.output.length > 0) {
+ out(UTF8ArrayToString(tty.output, 0));
+ tty.output = [];
+ }
+ }
+ },
+ default_tty1_ops: {
+ put_char: function(tty, val) {
+ if (val === null || val === 10) {
+ err(UTF8ArrayToString(tty.output, 0));
+ tty.output = [];
+ } else {
+ if (val != 0) tty.output.push(val);
+ }
+ },
+ flush: function(tty) {
+ if (tty.output && tty.output.length > 0) {
+ err(UTF8ArrayToString(tty.output, 0));
+ tty.output = [];
+ }
+ }
+ }
+};
+
+function alignMemory(size, alignment) {
+ assert(alignment, "alignment argument is required");
+ return Math.ceil(size / alignment) * alignment;
+}
+
+function mmapAlloc(size) {
+ size = alignMemory(size, 65536);
+ var ptr = _emscripten_builtin_memalign(65536, size);
+ if (!ptr) return 0;
+ zeroMemory(ptr, size);
+ return ptr;
+}
+
+var MEMFS = {
+ ops_table: null,
+ mount: function(mount) {
+ return MEMFS.createNode(null, "/", 16384 | 511, 0);
+ },
+ createNode: function(parent, name, mode, dev) {
+ if (FS.isBlkdev(mode) || FS.isFIFO(mode)) {
+ throw new FS.ErrnoError(63);
+ }
+ if (!MEMFS.ops_table) {
+ MEMFS.ops_table = {
+ dir: {
+ node: {
+ getattr: MEMFS.node_ops.getattr,
+ setattr: MEMFS.node_ops.setattr,
+ lookup: MEMFS.node_ops.lookup,
+ mknod: MEMFS.node_ops.mknod,
+ rename: MEMFS.node_ops.rename,
+ unlink: MEMFS.node_ops.unlink,
+ rmdir: MEMFS.node_ops.rmdir,
+ readdir: MEMFS.node_ops.readdir,
+ symlink: MEMFS.node_ops.symlink
+ },
+ stream: {
+ llseek: MEMFS.stream_ops.llseek
+ }
+ },
+ file: {
+ node: {
+ getattr: MEMFS.node_ops.getattr,
+ setattr: MEMFS.node_ops.setattr
+ },
+ stream: {
+ llseek: MEMFS.stream_ops.llseek,
+ read: MEMFS.stream_ops.read,
+ write: MEMFS.stream_ops.write,
+ allocate: MEMFS.stream_ops.allocate,
+ mmap: MEMFS.stream_ops.mmap,
+ msync: MEMFS.stream_ops.msync
+ }
+ },
+ link: {
+ node: {
+ getattr: MEMFS.node_ops.getattr,
+ setattr: MEMFS.node_ops.setattr,
+ readlink: MEMFS.node_ops.readlink
+ },
+ stream: {}
+ },
+ chrdev: {
+ node: {
+ getattr: MEMFS.node_ops.getattr,
+ setattr: MEMFS.node_ops.setattr
+ },
+ stream: FS.chrdev_stream_ops
+ }
+ };
+ }
+ var node = FS.createNode(parent, name, mode, dev);
+ if (FS.isDir(node.mode)) {
+ node.node_ops = MEMFS.ops_table.dir.node;
+ node.stream_ops = MEMFS.ops_table.dir.stream;
+ node.contents = {};
+ } else if (FS.isFile(node.mode)) {
+ node.node_ops = MEMFS.ops_table.file.node;
+ node.stream_ops = MEMFS.ops_table.file.stream;
+ node.usedBytes = 0;
+ node.contents = null;
+ } else if (FS.isLink(node.mode)) {
+ node.node_ops = MEMFS.ops_table.link.node;
+ node.stream_ops = MEMFS.ops_table.link.stream;
+ } else if (FS.isChrdev(node.mode)) {
+ node.node_ops = MEMFS.ops_table.chrdev.node;
+ node.stream_ops = MEMFS.ops_table.chrdev.stream;
+ }
+ node.timestamp = Date.now();
+ if (parent) {
+ parent.contents[name] = node;
+ parent.timestamp = node.timestamp;
+ }
+ return node;
+ },
+ getFileDataAsTypedArray: function(node) {
+ if (!node.contents) return new Uint8Array(0);
+ if (node.contents.subarray) return node.contents.subarray(0, node.usedBytes);
+ return new Uint8Array(node.contents);
+ },
+ expandFileStorage: function(node, newCapacity) {
+ var prevCapacity = node.contents ? node.contents.length : 0;
+ if (prevCapacity >= newCapacity) return;
+ var CAPACITY_DOUBLING_MAX = 1024 * 1024;
+ newCapacity = Math.max(newCapacity, prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2 : 1.125) >>> 0);
+ if (prevCapacity != 0) newCapacity = Math.max(newCapacity, 256);
+ var oldContents = node.contents;
+ node.contents = new Uint8Array(newCapacity);
+ if (node.usedBytes > 0) node.contents.set(oldContents.subarray(0, node.usedBytes), 0);
+ },
+ resizeFileStorage: function(node, newSize) {
+ if (node.usedBytes == newSize) return;
+ if (newSize == 0) {
+ node.contents = null;
+ node.usedBytes = 0;
+ } else {
+ var oldContents = node.contents;
+ node.contents = new Uint8Array(newSize);
+ if (oldContents) {
+ node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes)));
+ }
+ node.usedBytes = newSize;
+ }
+ },
+ node_ops: {
+ getattr: function(node) {
+ var attr = {};
+ attr.dev = FS.isChrdev(node.mode) ? node.id : 1;
+ attr.ino = node.id;
+ attr.mode = node.mode;
+ attr.nlink = 1;
+ attr.uid = 0;
+ attr.gid = 0;
+ attr.rdev = node.rdev;
+ if (FS.isDir(node.mode)) {
+ attr.size = 4096;
+ } else if (FS.isFile(node.mode)) {
+ attr.size = node.usedBytes;
+ } else if (FS.isLink(node.mode)) {
+ attr.size = node.link.length;
+ } else {
+ attr.size = 0;
+ }
+ attr.atime = new Date(node.timestamp);
+ attr.mtime = new Date(node.timestamp);
+ attr.ctime = new Date(node.timestamp);
+ attr.blksize = 4096;
+ attr.blocks = Math.ceil(attr.size / attr.blksize);
+ return attr;
+ },
+ setattr: function(node, attr) {
+ if (attr.mode !== undefined) {
+ node.mode = attr.mode;
+ }
+ if (attr.timestamp !== undefined) {
+ node.timestamp = attr.timestamp;
+ }
+ if (attr.size !== undefined) {
+ MEMFS.resizeFileStorage(node, attr.size);
+ }
+ },
+ lookup: function(parent, name) {
+ throw FS.genericErrors[44];
+ },
+ mknod: function(parent, name, mode, dev) {
+ return MEMFS.createNode(parent, name, mode, dev);
+ },
+ rename: function(old_node, new_dir, new_name) {
+ if (FS.isDir(old_node.mode)) {
+ var new_node;
+ try {
+ new_node = FS.lookupNode(new_dir, new_name);
+ } catch (e) {}
+ if (new_node) {
+ for (var i in new_node.contents) {
+ throw new FS.ErrnoError(55);
+ }
+ }
+ }
+ delete old_node.parent.contents[old_node.name];
+ old_node.parent.timestamp = Date.now();
+ old_node.name = new_name;
+ new_dir.contents[new_name] = old_node;
+ new_dir.timestamp = old_node.parent.timestamp;
+ old_node.parent = new_dir;
+ },
+ unlink: function(parent, name) {
+ delete parent.contents[name];
+ parent.timestamp = Date.now();
+ },
+ rmdir: function(parent, name) {
+ var node = FS.lookupNode(parent, name);
+ for (var i in node.contents) {
+ throw new FS.ErrnoError(55);
+ }
+ delete parent.contents[name];
+ parent.timestamp = Date.now();
+ },
+ readdir: function(node) {
+ var entries = [ ".", ".." ];
+ for (var key in node.contents) {
+ if (!node.contents.hasOwnProperty(key)) {
+ continue;
+ }
+ entries.push(key);
+ }
+ return entries;
+ },
+ symlink: function(parent, newname, oldpath) {
+ var node = MEMFS.createNode(parent, newname, 511 | 40960, 0);
+ node.link = oldpath;
+ return node;
+ },
+ readlink: function(node) {
+ if (!FS.isLink(node.mode)) {
+ throw new FS.ErrnoError(28);
+ }
+ return node.link;
+ }
+ },
+ stream_ops: {
+ read: function(stream, buffer, offset, length, position) {
+ var contents = stream.node.contents;
+ if (position >= stream.node.usedBytes) return 0;
+ var size = Math.min(stream.node.usedBytes - position, length);
+ assert(size >= 0);
+ if (size > 8 && contents.subarray) {
+ buffer.set(contents.subarray(position, position + size), offset);
+ } else {
+ for (var i = 0; i < size; i++) buffer[offset + i] = contents[position + i];
+ }
+ return size;
+ },
+ write: function(stream, buffer, offset, length, position, canOwn) {
+ assert(!(buffer instanceof ArrayBuffer));
+ if (buffer.buffer === GROWABLE_HEAP_I8().buffer) {
+ canOwn = false;
+ }
+ if (!length) return 0;
+ var node = stream.node;
+ node.timestamp = Date.now();
+ if (buffer.subarray && (!node.contents || node.contents.subarray)) {
+ if (canOwn) {
+ assert(position === 0, "canOwn must imply no weird position inside the file");
+ node.contents = buffer.subarray(offset, offset + length);
+ node.usedBytes = length;
+ return length;
+ } else if (node.usedBytes === 0 && position === 0) {
+ node.contents = buffer.slice(offset, offset + length);
+ node.usedBytes = length;
+ return length;
+ } else if (position + length <= node.usedBytes) {
+ node.contents.set(buffer.subarray(offset, offset + length), position);
+ return length;
+ }
+ }
+ MEMFS.expandFileStorage(node, position + length);
+ if (node.contents.subarray && buffer.subarray) {
+ node.contents.set(buffer.subarray(offset, offset + length), position);
+ } else {
+ for (var i = 0; i < length; i++) {
+ node.contents[position + i] = buffer[offset + i];
+ }
+ }
+ node.usedBytes = Math.max(node.usedBytes, position + length);
+ return length;
+ },
+ llseek: function(stream, offset, whence) {
+ var position = offset;
+ if (whence === 1) {
+ position += stream.position;
+ } else if (whence === 2) {
+ if (FS.isFile(stream.node.mode)) {
+ position += stream.node.usedBytes;
+ }
+ }
+ if (position < 0) {
+ throw new FS.ErrnoError(28);
+ }
+ return position;
+ },
+ allocate: function(stream, offset, length) {
+ MEMFS.expandFileStorage(stream.node, offset + length);
+ stream.node.usedBytes = Math.max(stream.node.usedBytes, offset + length);
+ },
+ mmap: function(stream, address, length, position, prot, flags) {
+ if (address !== 0) {
+ throw new FS.ErrnoError(28);
+ }
+ if (!FS.isFile(stream.node.mode)) {
+ throw new FS.ErrnoError(43);
+ }
+ var ptr;
+ var allocated;
+ var contents = stream.node.contents;
+ if (!(flags & 2) && contents.buffer === buffer) {
+ allocated = false;
+ ptr = contents.byteOffset;
+ } else {
+ if (position > 0 || position + length < contents.length) {
+ if (contents.subarray) {
+ contents = contents.subarray(position, position + length);
+ } else {
+ contents = Array.prototype.slice.call(contents, position, position + length);
+ }
+ }
+ allocated = true;
+ ptr = mmapAlloc(length);
+ if (!ptr) {
+ throw new FS.ErrnoError(48);
+ }
+ GROWABLE_HEAP_I8().set(contents, ptr);
+ }
+ return {
+ ptr: ptr,
+ allocated: allocated
+ };
+ },
+ msync: function(stream, buffer, offset, length, mmapFlags) {
+ if (!FS.isFile(stream.node.mode)) {
+ throw new FS.ErrnoError(43);
+ }
+ if (mmapFlags & 2) {
+ return 0;
+ }
+ var bytesWritten = MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false);
+ return 0;
+ }
+ }
+};
+
+function asyncLoad(url, onload, onerror, noRunDep) {
+ var dep = !noRunDep ? getUniqueRunDependency("al " + url) : "";
+ readAsync(url, function(arrayBuffer) {
+ assert(arrayBuffer, 'Loading data file "' + url + '" failed (no arrayBuffer).');
+ onload(new Uint8Array(arrayBuffer));
+ if (dep) removeRunDependency(dep);
+ }, function(event) {
+ if (onerror) {
+ onerror();
+ } else {
+ throw 'Loading data file "' + url + '" failed.';
+ }
+ });
+ if (dep) addRunDependency(dep);
+}
+
+var IDBFS = {
+ dbs: {},
+ indexedDB: () => {
+ if (typeof indexedDB != "undefined") return indexedDB;
+ var ret = null;
+ if (typeof window == "object") ret = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
+ assert(ret, "IDBFS used, but indexedDB not supported");
+ return ret;
+ },
+ DB_VERSION: 21,
+ DB_STORE_NAME: "FILE_DATA",
+ mount: function(mount) {
+ return MEMFS.mount.apply(null, arguments);
+ },
+ syncfs: (mount, populate, callback) => {
+ IDBFS.getLocalSet(mount, (err, local) => {
+ if (err) return callback(err);
+ IDBFS.getRemoteSet(mount, (err, remote) => {
+ if (err) return callback(err);
+ var src = populate ? remote : local;
+ var dst = populate ? local : remote;
+ IDBFS.reconcile(src, dst, callback);
+ });
+ });
+ },
+ getDB: (name, callback) => {
+ var db = IDBFS.dbs[name];
+ if (db) {
+ return callback(null, db);
+ }
+ var req;
+ try {
+ req = IDBFS.indexedDB().open(name, IDBFS.DB_VERSION);
+ } catch (e) {
+ return callback(e);
+ }
+ if (!req) {
+ return callback("Unable to connect to IndexedDB");
+ }
+ req.onupgradeneeded = (e => {
+ var db = e.target.result;
+ var transaction = e.target.transaction;
+ var fileStore;
+ if (db.objectStoreNames.contains(IDBFS.DB_STORE_NAME)) {
+ fileStore = transaction.objectStore(IDBFS.DB_STORE_NAME);
+ } else {
+ fileStore = db.createObjectStore(IDBFS.DB_STORE_NAME);
+ }
+ if (!fileStore.indexNames.contains("timestamp")) {
+ fileStore.createIndex("timestamp", "timestamp", {
+ unique: false
+ });
+ }
+ });
+ req.onsuccess = (() => {
+ db = req.result;
+ IDBFS.dbs[name] = db;
+ callback(null, db);
+ });
+ req.onerror = (e => {
+ callback(this.error);
+ e.preventDefault();
+ });
+ },
+ getLocalSet: (mount, callback) => {
+ var entries = {};
+ function isRealDir(p) {
+ return p !== "." && p !== "..";
+ }
+ function toAbsolute(root) {
+ return p => {
+ return PATH.join2(root, p);
+ };
+ }
+ var check = FS.readdir(mount.mountpoint).filter(isRealDir).map(toAbsolute(mount.mountpoint));
+ while (check.length) {
+ var path = check.pop();
+ var stat;
+ try {
+ stat = FS.stat(path);
+ } catch (e) {
+ return callback(e);
+ }
+ if (FS.isDir(stat.mode)) {
+ check.push.apply(check, FS.readdir(path).filter(isRealDir).map(toAbsolute(path)));
+ }
+ entries[path] = {
+ "timestamp": stat.mtime
+ };
+ }
+ return callback(null, {
+ type: "local",
+ entries: entries
+ });
+ },
+ getRemoteSet: (mount, callback) => {
+ var entries = {};
+ IDBFS.getDB(mount.mountpoint, (err, db) => {
+ if (err) return callback(err);
+ try {
+ var transaction = db.transaction([ IDBFS.DB_STORE_NAME ], "readonly");
+ transaction.onerror = (e => {
+ callback(this.error);
+ e.preventDefault();
+ });
+ var store = transaction.objectStore(IDBFS.DB_STORE_NAME);
+ var index = store.index("timestamp");
+ index.openKeyCursor().onsuccess = (event => {
+ var cursor = event.target.result;
+ if (!cursor) {
+ return callback(null, {
+ type: "remote",
+ db: db,
+ entries: entries
+ });
+ }
+ entries[cursor.primaryKey] = {
+ "timestamp": cursor.key
+ };
+ cursor.continue();
+ });
+ } catch (e) {
+ return callback(e);
+ }
+ });
+ },
+ loadLocalEntry: (path, callback) => {
+ var stat, node;
+ try {
+ var lookup = FS.lookupPath(path);
+ node = lookup.node;
+ stat = FS.stat(path);
+ } catch (e) {
+ return callback(e);
+ }
+ if (FS.isDir(stat.mode)) {
+ return callback(null, {
+ "timestamp": stat.mtime,
+ "mode": stat.mode
+ });
+ } else if (FS.isFile(stat.mode)) {
+ node.contents = MEMFS.getFileDataAsTypedArray(node);
+ return callback(null, {
+ "timestamp": stat.mtime,
+ "mode": stat.mode,
+ "contents": node.contents
+ });
+ } else {
+ return callback(new Error("node type not supported"));
+ }
+ },
+ storeLocalEntry: (path, entry, callback) => {
+ try {
+ if (FS.isDir(entry["mode"])) {
+ FS.mkdirTree(path, entry["mode"]);
+ } else if (FS.isFile(entry["mode"])) {
+ FS.writeFile(path, entry["contents"], {
+ canOwn: true
+ });
+ } else {
+ return callback(new Error("node type not supported"));
+ }
+ FS.chmod(path, entry["mode"]);
+ FS.utime(path, entry["timestamp"], entry["timestamp"]);
+ } catch (e) {
+ return callback(e);
+ }
+ callback(null);
+ },
+ removeLocalEntry: (path, callback) => {
+ try {
+ var lookup = FS.lookupPath(path);
+ var stat = FS.stat(path);
+ if (FS.isDir(stat.mode)) {
+ FS.rmdir(path);
+ } else if (FS.isFile(stat.mode)) {
+ FS.unlink(path);
+ }
+ } catch (e) {
+ return callback(e);
+ }
+ callback(null);
+ },
+ loadRemoteEntry: (store, path, callback) => {
+ var req = store.get(path);
+ req.onsuccess = (event => {
+ callback(null, event.target.result);
+ });
+ req.onerror = (e => {
+ callback(this.error);
+ e.preventDefault();
+ });
+ },
+ storeRemoteEntry: (store, path, entry, callback) => {
+ try {
+ var req = store.put(entry, path);
+ } catch (e) {
+ callback(e);
+ return;
+ }
+ req.onsuccess = (() => {
+ callback(null);
+ });
+ req.onerror = (e => {
+ callback(this.error);
+ e.preventDefault();
+ });
+ },
+ removeRemoteEntry: (store, path, callback) => {
+ var req = store.delete(path);
+ req.onsuccess = (() => {
+ callback(null);
+ });
+ req.onerror = (e => {
+ callback(this.error);
+ e.preventDefault();
+ });
+ },
+ reconcile: (src, dst, callback) => {
+ var total = 0;
+ var create = [];
+ Object.keys(src.entries).forEach(function(key) {
+ var e = src.entries[key];
+ var e2 = dst.entries[key];
+ if (!e2 || e["timestamp"].getTime() != e2["timestamp"].getTime()) {
+ create.push(key);
+ total++;
+ }
+ });
+ var remove = [];
+ Object.keys(dst.entries).forEach(function(key) {
+ if (!src.entries[key]) {
+ remove.push(key);
+ total++;
+ }
+ });
+ if (!total) {
+ return callback(null);
+ }
+ var errored = false;
+ var db = src.type === "remote" ? src.db : dst.db;
+ var transaction = db.transaction([ IDBFS.DB_STORE_NAME ], "readwrite");
+ var store = transaction.objectStore(IDBFS.DB_STORE_NAME);
+ function done(err) {
+ if (err && !errored) {
+ errored = true;
+ return callback(err);
+ }
+ }
+ transaction.onerror = (e => {
+ done(this.error);
+ e.preventDefault();
+ });
+ transaction.oncomplete = (e => {
+ if (!errored) {
+ callback(null);
+ }
+ });
+ create.sort().forEach(path => {
+ if (dst.type === "local") {
+ IDBFS.loadRemoteEntry(store, path, (err, entry) => {
+ if (err) return done(err);
+ IDBFS.storeLocalEntry(path, entry, done);
+ });
+ } else {
+ IDBFS.loadLocalEntry(path, (err, entry) => {
+ if (err) return done(err);
+ IDBFS.storeRemoteEntry(store, path, entry, done);
+ });
+ }
+ });
+ remove.sort().reverse().forEach(path => {
+ if (dst.type === "local") {
+ IDBFS.removeLocalEntry(path, done);
+ } else {
+ IDBFS.removeRemoteEntry(store, path, done);
+ }
+ });
+ }
+};
+
+var ERRNO_MESSAGES = {
+ 0: "Success",
+ 1: "Arg list too long",
+ 2: "Permission denied",
+ 3: "Address already in use",
+ 4: "Address not available",
+ 5: "Address family not supported by protocol family",
+ 6: "No more processes",
+ 7: "Socket already connected",
+ 8: "Bad file number",
+ 9: "Trying to read unreadable message",
+ 10: "Mount device busy",
+ 11: "Operation canceled",
+ 12: "No children",
+ 13: "Connection aborted",
+ 14: "Connection refused",
+ 15: "Connection reset by peer",
+ 16: "File locking deadlock error",
+ 17: "Destination address required",
+ 18: "Math arg out of domain of func",
+ 19: "Quota exceeded",
+ 20: "File exists",
+ 21: "Bad address",
+ 22: "File too large",
+ 23: "Host is unreachable",
+ 24: "Identifier removed",
+ 25: "Illegal byte sequence",
+ 26: "Connection already in progress",
+ 27: "Interrupted system call",
+ 28: "Invalid argument",
+ 29: "I/O error",
+ 30: "Socket is already connected",
+ 31: "Is a directory",
+ 32: "Too many symbolic links",
+ 33: "Too many open files",
+ 34: "Too many links",
+ 35: "Message too long",
+ 36: "Multihop attempted",
+ 37: "File or path name too long",
+ 38: "Network interface is not configured",
+ 39: "Connection reset by network",
+ 40: "Network is unreachable",
+ 41: "Too many open files in system",
+ 42: "No buffer space available",
+ 43: "No such device",
+ 44: "No such file or directory",
+ 45: "Exec format error",
+ 46: "No record locks available",
+ 47: "The link has been severed",
+ 48: "Not enough core",
+ 49: "No message of desired type",
+ 50: "Protocol not available",
+ 51: "No space left on device",
+ 52: "Function not implemented",
+ 53: "Socket is not connected",
+ 54: "Not a directory",
+ 55: "Directory not empty",
+ 56: "State not recoverable",
+ 57: "Socket operation on non-socket",
+ 59: "Not a typewriter",
+ 60: "No such device or address",
+ 61: "Value too large for defined data type",
+ 62: "Previous owner died",
+ 63: "Not super-user",
+ 64: "Broken pipe",
+ 65: "Protocol error",
+ 66: "Unknown protocol",
+ 67: "Protocol wrong type for socket",
+ 68: "Math result not representable",
+ 69: "Read only file system",
+ 70: "Illegal seek",
+ 71: "No such process",
+ 72: "Stale file handle",
+ 73: "Connection timed out",
+ 74: "Text file busy",
+ 75: "Cross-device link",
+ 100: "Device not a stream",
+ 101: "Bad font file fmt",
+ 102: "Invalid slot",
+ 103: "Invalid request code",
+ 104: "No anode",
+ 105: "Block device required",
+ 106: "Channel number out of range",
+ 107: "Level 3 halted",
+ 108: "Level 3 reset",
+ 109: "Link number out of range",
+ 110: "Protocol driver not attached",
+ 111: "No CSI structure available",
+ 112: "Level 2 halted",
+ 113: "Invalid exchange",
+ 114: "Invalid request descriptor",
+ 115: "Exchange full",
+ 116: "No data (for no delay io)",
+ 117: "Timer expired",
+ 118: "Out of streams resources",
+ 119: "Machine is not on the network",
+ 120: "Package not installed",
+ 121: "The object is remote",
+ 122: "Advertise error",
+ 123: "Srmount error",
+ 124: "Communication error on send",
+ 125: "Cross mount point (not really error)",
+ 126: "Given log. name not unique",
+ 127: "f.d. invalid for this operation",
+ 128: "Remote address changed",
+ 129: "Can access a needed shared lib",
+ 130: "Accessing a corrupted shared lib",
+ 131: ".lib section in a.out corrupted",
+ 132: "Attempting to link in too many libs",
+ 133: "Attempting to exec a shared library",
+ 135: "Streams pipe error",
+ 136: "Too many users",
+ 137: "Socket type not supported",
+ 138: "Not supported",
+ 139: "Protocol family not supported",
+ 140: "Can't send after socket shutdown",
+ 141: "Too many references",
+ 142: "Host is down",
+ 148: "No medium (in tape drive)",
+ 156: "Level 2 not synchronized"
+};
+
+var ERRNO_CODES = {};
+
+var FS = {
+ root: null,
+ mounts: [],
+ devices: {},
+ streams: [],
+ nextInode: 1,
+ nameTable: null,
+ currentPath: "/",
+ initialized: false,
+ ignorePermissions: true,
+ ErrnoError: null,
+ genericErrors: {},
+ filesystems: null,
+ syncFSRequests: 0,
+ lookupPath: (path, opts = {}) => {
+ path = PATH_FS.resolve(FS.cwd(), path);
+ if (!path) return {
+ path: "",
+ node: null
+ };
+ var defaults = {
+ follow_mount: true,
+ recurse_count: 0
+ };
+ for (var key in defaults) {
+ if (opts[key] === undefined) {
+ opts[key] = defaults[key];
+ }
+ }
+ if (opts.recurse_count > 8) {
+ throw new FS.ErrnoError(32);
+ }
+ var parts = PATH.normalizeArray(path.split("/").filter(p => !!p), false);
+ var current = FS.root;
+ var current_path = "/";
+ for (var i = 0; i < parts.length; i++) {
+ var islast = i === parts.length - 1;
+ if (islast && opts.parent) {
+ break;
+ }
+ current = FS.lookupNode(current, parts[i]);
+ current_path = PATH.join2(current_path, parts[i]);
+ if (FS.isMountpoint(current)) {
+ if (!islast || islast && opts.follow_mount) {
+ current = current.mounted.root;
+ }
+ }
+ if (!islast || opts.follow) {
+ var count = 0;
+ while (FS.isLink(current.mode)) {
+ var link = FS.readlink(current_path);
+ current_path = PATH_FS.resolve(PATH.dirname(current_path), link);
+ var lookup = FS.lookupPath(current_path, {
+ recurse_count: opts.recurse_count
+ });
+ current = lookup.node;
+ if (count++ > 40) {
+ throw new FS.ErrnoError(32);
+ }
+ }
+ }
+ }
+ return {
+ path: current_path,
+ node: current
+ };
+ },
+ getPath: node => {
+ var path;
+ while (true) {
+ if (FS.isRoot(node)) {
+ var mount = node.mount.mountpoint;
+ if (!path) return mount;
+ return mount[mount.length - 1] !== "/" ? mount + "/" + path : mount + path;
+ }
+ path = path ? node.name + "/" + path : node.name;
+ node = node.parent;
+ }
+ },
+ hashName: (parentid, name) => {
+ var hash = 0;
+ for (var i = 0; i < name.length; i++) {
+ hash = (hash << 5) - hash + name.charCodeAt(i) | 0;
+ }
+ return (parentid + hash >>> 0) % FS.nameTable.length;
+ },
+ hashAddNode: node => {
+ var hash = FS.hashName(node.parent.id, node.name);
+ node.name_next = FS.nameTable[hash];
+ FS.nameTable[hash] = node;
+ },
+ hashRemoveNode: node => {
+ var hash = FS.hashName(node.parent.id, node.name);
+ if (FS.nameTable[hash] === node) {
+ FS.nameTable[hash] = node.name_next;
+ } else {
+ var current = FS.nameTable[hash];
+ while (current) {
+ if (current.name_next === node) {
+ current.name_next = node.name_next;
+ break;
+ }
+ current = current.name_next;
+ }
+ }
+ },
+ lookupNode: (parent, name) => {
+ var errCode = FS.mayLookup(parent);
+ if (errCode) {
+ throw new FS.ErrnoError(errCode, parent);
+ }
+ var hash = FS.hashName(parent.id, name);
+ for (var node = FS.nameTable[hash]; node; node = node.name_next) {
+ var nodeName = node.name;
+ if (node.parent.id === parent.id && nodeName === name) {
+ return node;
+ }
+ }
+ return FS.lookup(parent, name);
+ },
+ createNode: (parent, name, mode, rdev) => {
+ assert(typeof parent == "object");
+ var node = new FS.FSNode(parent, name, mode, rdev);
+ FS.hashAddNode(node);
+ return node;
+ },
+ destroyNode: node => {
+ FS.hashRemoveNode(node);
+ },
+ isRoot: node => {
+ return node === node.parent;
+ },
+ isMountpoint: node => {
+ return !!node.mounted;
+ },
+ isFile: mode => {
+ return (mode & 61440) === 32768;
+ },
+ isDir: mode => {
+ return (mode & 61440) === 16384;
+ },
+ isLink: mode => {
+ return (mode & 61440) === 40960;
+ },
+ isChrdev: mode => {
+ return (mode & 61440) === 8192;
+ },
+ isBlkdev: mode => {
+ return (mode & 61440) === 24576;
+ },
+ isFIFO: mode => {
+ return (mode & 61440) === 4096;
+ },
+ isSocket: mode => {
+ return (mode & 49152) === 49152;
+ },
+ flagModes: {
+ "r": 0,
+ "r+": 2,
+ "w": 577,
+ "w+": 578,
+ "a": 1089,
+ "a+": 1090
+ },
+ modeStringToFlags: str => {
+ var flags = FS.flagModes[str];
+ if (typeof flags == "undefined") {
+ throw new Error("Unknown file open mode: " + str);
+ }
+ return flags;
+ },
+ flagsToPermissionString: flag => {
+ var perms = [ "r", "w", "rw" ][flag & 3];
+ if (flag & 512) {
+ perms += "w";
+ }
+ return perms;
+ },
+ nodePermissions: (node, perms) => {
+ if (FS.ignorePermissions) {
+ return 0;
+ }
+ if (perms.includes("r") && !(node.mode & 292)) {
+ return 2;
+ } else if (perms.includes("w") && !(node.mode & 146)) {
+ return 2;
+ } else if (perms.includes("x") && !(node.mode & 73)) {
+ return 2;
+ }
+ return 0;
+ },
+ mayLookup: dir => {
+ var errCode = FS.nodePermissions(dir, "x");
+ if (errCode) return errCode;
+ if (!dir.node_ops.lookup) return 2;
+ return 0;
+ },
+ mayCreate: (dir, name) => {
+ try {
+ var node = FS.lookupNode(dir, name);
+ return 20;
+ } catch (e) {}
+ return FS.nodePermissions(dir, "wx");
+ },
+ mayDelete: (dir, name, isdir) => {
+ var node;
+ try {
+ node = FS.lookupNode(dir, name);
+ } catch (e) {
+ return e.errno;
+ }
+ var errCode = FS.nodePermissions(dir, "wx");
+ if (errCode) {
+ return errCode;
+ }
+ if (isdir) {
+ if (!FS.isDir(node.mode)) {
+ return 54;
+ }
+ if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) {
+ return 10;
+ }
+ } else {
+ if (FS.isDir(node.mode)) {
+ return 31;
+ }
+ }
+ return 0;
+ },
+ mayOpen: (node, flags) => {
+ if (!node) {
+ return 44;
+ }
+ if (FS.isLink(node.mode)) {
+ return 32;
+ } else if (FS.isDir(node.mode)) {
+ if (FS.flagsToPermissionString(flags) !== "r" || flags & 512) {
+ return 31;
+ }
+ }
+ return FS.nodePermissions(node, FS.flagsToPermissionString(flags));
+ },
+ MAX_OPEN_FDS: 4096,
+ nextfd: (fd_start = 0, fd_end = FS.MAX_OPEN_FDS) => {
+ for (var fd = fd_start; fd <= fd_end; fd++) {
+ if (!FS.streams[fd]) {
+ return fd;
+ }
+ }
+ throw new FS.ErrnoError(33);
+ },
+ getStream: fd => FS.streams[fd],
+ createStream: (stream, fd_start, fd_end) => {
+ if (!FS.FSStream) {
+ FS.FSStream = function() {};
+ FS.FSStream.prototype = {
+ object: {
+ get: function() {
+ return this.node;
+ },
+ set: function(val) {
+ this.node = val;
+ }
+ },
+ isRead: {
+ get: function() {
+ return (this.flags & 2097155) !== 1;
+ }
+ },
+ isWrite: {
+ get: function() {
+ return (this.flags & 2097155) !== 0;
+ }
+ },
+ isAppend: {
+ get: function() {
+ return this.flags & 1024;
+ }
+ }
+ };
+ }
+ stream = Object.assign(new FS.FSStream(), stream);
+ var fd = FS.nextfd(fd_start, fd_end);
+ stream.fd = fd;
+ FS.streams[fd] = stream;
+ return stream;
+ },
+ closeStream: fd => {
+ FS.streams[fd] = null;
+ },
+ chrdev_stream_ops: {
+ open: stream => {
+ var device = FS.getDevice(stream.node.rdev);
+ stream.stream_ops = device.stream_ops;
+ if (stream.stream_ops.open) {
+ stream.stream_ops.open(stream);
+ }
+ },
+ llseek: () => {
+ throw new FS.ErrnoError(70);
+ }
+ },
+ major: dev => dev >> 8,
+ minor: dev => dev & 255,
+ makedev: (ma, mi) => ma << 8 | mi,
+ registerDevice: (dev, ops) => {
+ FS.devices[dev] = {
+ stream_ops: ops
+ };
+ },
+ getDevice: dev => FS.devices[dev],
+ getMounts: mount => {
+ var mounts = [];
+ var check = [ mount ];
+ while (check.length) {
+ var m = check.pop();
+ mounts.push(m);
+ check.push.apply(check, m.mounts);
+ }
+ return mounts;
+ },
+ syncfs: (populate, callback) => {
+ if (typeof populate == "function") {
+ callback = populate;
+ populate = false;
+ }
+ FS.syncFSRequests++;
+ if (FS.syncFSRequests > 1) {
+ err("warning: " + FS.syncFSRequests + " FS.syncfs operations in flight at once, probably just doing extra work");
+ }
+ var mounts = FS.getMounts(FS.root.mount);
+ var completed = 0;
+ function doCallback(errCode) {
+ assert(FS.syncFSRequests > 0);
+ FS.syncFSRequests--;
+ return callback(errCode);
+ }
+ function done(errCode) {
+ if (errCode) {
+ if (!done.errored) {
+ done.errored = true;
+ return doCallback(errCode);
+ }
+ return;
+ }
+ if (++completed >= mounts.length) {
+ doCallback(null);
+ }
+ }
+ mounts.forEach(mount => {
+ if (!mount.type.syncfs) {
+ return done(null);
+ }
+ mount.type.syncfs(mount, populate, done);
+ });
+ },
+ mount: (type, opts, mountpoint) => {
+ if (typeof type == "string") {
+ throw type;
+ }
+ var root = mountpoint === "/";
+ var pseudo = !mountpoint;
+ var node;
+ if (root && FS.root) {
+ throw new FS.ErrnoError(10);
+ } else if (!root && !pseudo) {
+ var lookup = FS.lookupPath(mountpoint, {
+ follow_mount: false
+ });
+ mountpoint = lookup.path;
+ node = lookup.node;
+ if (FS.isMountpoint(node)) {
+ throw new FS.ErrnoError(10);
+ }
+ if (!FS.isDir(node.mode)) {
+ throw new FS.ErrnoError(54);
+ }
+ }
+ var mount = {
+ type: type,
+ opts: opts,
+ mountpoint: mountpoint,
+ mounts: []
+ };
+ var mountRoot = type.mount(mount);
+ mountRoot.mount = mount;
+ mount.root = mountRoot;
+ if (root) {
+ FS.root = mountRoot;
+ } else if (node) {
+ node.mounted = mount;
+ if (node.mount) {
+ node.mount.mounts.push(mount);
+ }
+ }
+ return mountRoot;
+ },
+ unmount: mountpoint => {
+ var lookup = FS.lookupPath(mountpoint, {
+ follow_mount: false
+ });
+ if (!FS.isMountpoint(lookup.node)) {
+ throw new FS.ErrnoError(28);
+ }
+ var node = lookup.node;
+ var mount = node.mounted;
+ var mounts = FS.getMounts(mount);
+ Object.keys(FS.nameTable).forEach(hash => {
+ var current = FS.nameTable[hash];
+ while (current) {
+ var next = current.name_next;
+ if (mounts.includes(current.mount)) {
+ FS.destroyNode(current);
+ }
+ current = next;
+ }
+ });
+ node.mounted = null;
+ var idx = node.mount.mounts.indexOf(mount);
+ assert(idx !== -1);
+ node.mount.mounts.splice(idx, 1);
+ },
+ lookup: (parent, name) => {
+ return parent.node_ops.lookup(parent, name);
+ },
+ mknod: (path, mode, dev) => {
+ var lookup = FS.lookupPath(path, {
+ parent: true
+ });
+ var parent = lookup.node;
+ var name = PATH.basename(path);
+ if (!name || name === "." || name === "..") {
+ throw new FS.ErrnoError(28);
+ }
+ var errCode = FS.mayCreate(parent, name);
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ if (!parent.node_ops.mknod) {
+ throw new FS.ErrnoError(63);
+ }
+ return parent.node_ops.mknod(parent, name, mode, dev);
+ },
+ create: (path, mode) => {
+ mode = mode !== undefined ? mode : 438;
+ mode &= 4095;
+ mode |= 32768;
+ return FS.mknod(path, mode, 0);
+ },
+ mkdir: (path, mode) => {
+ mode = mode !== undefined ? mode : 511;
+ mode &= 511 | 512;
+ mode |= 16384;
+ return FS.mknod(path, mode, 0);
+ },
+ mkdirTree: (path, mode) => {
+ var dirs = path.split("/");
+ var d = "";
+ for (var i = 0; i < dirs.length; ++i) {
+ if (!dirs[i]) continue;
+ d += "/" + dirs[i];
+ try {
+ FS.mkdir(d, mode);
+ } catch (e) {
+ if (e.errno != 20) throw e;
+ }
+ }
+ },
+ mkdev: (path, mode, dev) => {
+ if (typeof dev == "undefined") {
+ dev = mode;
+ mode = 438;
+ }
+ mode |= 8192;
+ return FS.mknod(path, mode, dev);
+ },
+ symlink: (oldpath, newpath) => {
+ if (!PATH_FS.resolve(oldpath)) {
+ throw new FS.ErrnoError(44);
+ }
+ var lookup = FS.lookupPath(newpath, {
+ parent: true
+ });
+ var parent = lookup.node;
+ if (!parent) {
+ throw new FS.ErrnoError(44);
+ }
+ var newname = PATH.basename(newpath);
+ var errCode = FS.mayCreate(parent, newname);
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ if (!parent.node_ops.symlink) {
+ throw new FS.ErrnoError(63);
+ }
+ return parent.node_ops.symlink(parent, newname, oldpath);
+ },
+ rename: (old_path, new_path) => {
+ var old_dirname = PATH.dirname(old_path);
+ var new_dirname = PATH.dirname(new_path);
+ var old_name = PATH.basename(old_path);
+ var new_name = PATH.basename(new_path);
+ var lookup, old_dir, new_dir;
+ lookup = FS.lookupPath(old_path, {
+ parent: true
+ });
+ old_dir = lookup.node;
+ lookup = FS.lookupPath(new_path, {
+ parent: true
+ });
+ new_dir = lookup.node;
+ if (!old_dir || !new_dir) throw new FS.ErrnoError(44);
+ if (old_dir.mount !== new_dir.mount) {
+ throw new FS.ErrnoError(75);
+ }
+ var old_node = FS.lookupNode(old_dir, old_name);
+ var relative = PATH_FS.relative(old_path, new_dirname);
+ if (relative.charAt(0) !== ".") {
+ throw new FS.ErrnoError(28);
+ }
+ relative = PATH_FS.relative(new_path, old_dirname);
+ if (relative.charAt(0) !== ".") {
+ throw new FS.ErrnoError(55);
+ }
+ var new_node;
+ try {
+ new_node = FS.lookupNode(new_dir, new_name);
+ } catch (e) {}
+ if (old_node === new_node) {
+ return;
+ }
+ var isdir = FS.isDir(old_node.mode);
+ var errCode = FS.mayDelete(old_dir, old_name, isdir);
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ errCode = new_node ? FS.mayDelete(new_dir, new_name, isdir) : FS.mayCreate(new_dir, new_name);
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ if (!old_dir.node_ops.rename) {
+ throw new FS.ErrnoError(63);
+ }
+ if (FS.isMountpoint(old_node) || new_node && FS.isMountpoint(new_node)) {
+ throw new FS.ErrnoError(10);
+ }
+ if (new_dir !== old_dir) {
+ errCode = FS.nodePermissions(old_dir, "w");
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ }
+ FS.hashRemoveNode(old_node);
+ try {
+ old_dir.node_ops.rename(old_node, new_dir, new_name);
+ } catch (e) {
+ throw e;
+ } finally {
+ FS.hashAddNode(old_node);
+ }
+ },
+ rmdir: path => {
+ var lookup = FS.lookupPath(path, {
+ parent: true
+ });
+ var parent = lookup.node;
+ var name = PATH.basename(path);
+ var node = FS.lookupNode(parent, name);
+ var errCode = FS.mayDelete(parent, name, true);
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ if (!parent.node_ops.rmdir) {
+ throw new FS.ErrnoError(63);
+ }
+ if (FS.isMountpoint(node)) {
+ throw new FS.ErrnoError(10);
+ }
+ parent.node_ops.rmdir(parent, name);
+ FS.destroyNode(node);
+ },
+ readdir: path => {
+ var lookup = FS.lookupPath(path, {
+ follow: true
+ });
+ var node = lookup.node;
+ if (!node.node_ops.readdir) {
+ throw new FS.ErrnoError(54);
+ }
+ return node.node_ops.readdir(node);
+ },
+ unlink: path => {
+ var lookup = FS.lookupPath(path, {
+ parent: true
+ });
+ var parent = lookup.node;
+ if (!parent) {
+ throw new FS.ErrnoError(44);
+ }
+ var name = PATH.basename(path);
+ var node = FS.lookupNode(parent, name);
+ var errCode = FS.mayDelete(parent, name, false);
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ if (!parent.node_ops.unlink) {
+ throw new FS.ErrnoError(63);
+ }
+ if (FS.isMountpoint(node)) {
+ throw new FS.ErrnoError(10);
+ }
+ parent.node_ops.unlink(parent, name);
+ FS.destroyNode(node);
+ },
+ readlink: path => {
+ var lookup = FS.lookupPath(path);
+ var link = lookup.node;
+ if (!link) {
+ throw new FS.ErrnoError(44);
+ }
+ if (!link.node_ops.readlink) {
+ throw new FS.ErrnoError(28);
+ }
+ return PATH_FS.resolve(FS.getPath(link.parent), link.node_ops.readlink(link));
+ },
+ stat: (path, dontFollow) => {
+ var lookup = FS.lookupPath(path, {
+ follow: !dontFollow
+ });
+ var node = lookup.node;
+ if (!node) {
+ throw new FS.ErrnoError(44);
+ }
+ if (!node.node_ops.getattr) {
+ throw new FS.ErrnoError(63);
+ }
+ return node.node_ops.getattr(node);
+ },
+ lstat: path => {
+ return FS.stat(path, true);
+ },
+ chmod: (path, mode, dontFollow) => {
+ var node;
+ if (typeof path == "string") {
+ var lookup = FS.lookupPath(path, {
+ follow: !dontFollow
+ });
+ node = lookup.node;
+ } else {
+ node = path;
+ }
+ if (!node.node_ops.setattr) {
+ throw new FS.ErrnoError(63);
+ }
+ node.node_ops.setattr(node, {
+ mode: mode & 4095 | node.mode & ~4095,
+ timestamp: Date.now()
+ });
+ },
+ lchmod: (path, mode) => {
+ FS.chmod(path, mode, true);
+ },
+ fchmod: (fd, mode) => {
+ var stream = FS.getStream(fd);
+ if (!stream) {
+ throw new FS.ErrnoError(8);
+ }
+ FS.chmod(stream.node, mode);
+ },
+ chown: (path, uid, gid, dontFollow) => {
+ var node;
+ if (typeof path == "string") {
+ var lookup = FS.lookupPath(path, {
+ follow: !dontFollow
+ });
+ node = lookup.node;
+ } else {
+ node = path;
+ }
+ if (!node.node_ops.setattr) {
+ throw new FS.ErrnoError(63);
+ }
+ node.node_ops.setattr(node, {
+ timestamp: Date.now()
+ });
+ },
+ lchown: (path, uid, gid) => {
+ FS.chown(path, uid, gid, true);
+ },
+ fchown: (fd, uid, gid) => {
+ var stream = FS.getStream(fd);
+ if (!stream) {
+ throw new FS.ErrnoError(8);
+ }
+ FS.chown(stream.node, uid, gid);
+ },
+ truncate: (path, len) => {
+ if (len < 0) {
+ throw new FS.ErrnoError(28);
+ }
+ var node;
+ if (typeof path == "string") {
+ var lookup = FS.lookupPath(path, {
+ follow: true
+ });
+ node = lookup.node;
+ } else {
+ node = path;
+ }
+ if (!node.node_ops.setattr) {
+ throw new FS.ErrnoError(63);
+ }
+ if (FS.isDir(node.mode)) {
+ throw new FS.ErrnoError(31);
+ }
+ if (!FS.isFile(node.mode)) {
+ throw new FS.ErrnoError(28);
+ }
+ var errCode = FS.nodePermissions(node, "w");
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ node.node_ops.setattr(node, {
+ size: len,
+ timestamp: Date.now()
+ });
+ },
+ ftruncate: (fd, len) => {
+ var stream = FS.getStream(fd);
+ if (!stream) {
+ throw new FS.ErrnoError(8);
+ }
+ if ((stream.flags & 2097155) === 0) {
+ throw new FS.ErrnoError(28);
+ }
+ FS.truncate(stream.node, len);
+ },
+ utime: (path, atime, mtime) => {
+ var lookup = FS.lookupPath(path, {
+ follow: true
+ });
+ var node = lookup.node;
+ node.node_ops.setattr(node, {
+ timestamp: Math.max(atime, mtime)
+ });
+ },
+ open: (path, flags, mode, fd_start, fd_end) => {
+ if (path === "") {
+ throw new FS.ErrnoError(44);
+ }
+ flags = typeof flags == "string" ? FS.modeStringToFlags(flags) : flags;
+ mode = typeof mode == "undefined" ? 438 : mode;
+ if (flags & 64) {
+ mode = mode & 4095 | 32768;
+ } else {
+ mode = 0;
+ }
+ var node;
+ if (typeof path == "object") {
+ node = path;
+ } else {
+ path = PATH.normalize(path);
+ try {
+ var lookup = FS.lookupPath(path, {
+ follow: !(flags & 131072)
+ });
+ node = lookup.node;
+ } catch (e) {}
+ }
+ var created = false;
+ if (flags & 64) {
+ if (node) {
+ if (flags & 128) {
+ throw new FS.ErrnoError(20);
+ }
+ } else {
+ node = FS.mknod(path, mode, 0);
+ created = true;
+ }
+ }
+ if (!node) {
+ throw new FS.ErrnoError(44);
+ }
+ if (FS.isChrdev(node.mode)) {
+ flags &= ~512;
+ }
+ if (flags & 65536 && !FS.isDir(node.mode)) {
+ throw new FS.ErrnoError(54);
+ }
+ if (!created) {
+ var errCode = FS.mayOpen(node, flags);
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ }
+ if (flags & 512) {
+ FS.truncate(node, 0);
+ }
+ flags &= ~(128 | 512 | 131072);
+ var stream = FS.createStream({
+ node: node,
+ path: FS.getPath(node),
+ flags: flags,
+ seekable: true,
+ position: 0,
+ stream_ops: node.stream_ops,
+ ungotten: [],
+ error: false
+ }, fd_start, fd_end);
+ if (stream.stream_ops.open) {
+ stream.stream_ops.open(stream);
+ }
+ if (Module["logReadFiles"] && !(flags & 1)) {
+ if (!FS.readFiles) FS.readFiles = {};
+ if (!(path in FS.readFiles)) {
+ FS.readFiles[path] = 1;
+ }
+ }
+ return stream;
+ },
+ close: stream => {
+ if (FS.isClosed(stream)) {
+ throw new FS.ErrnoError(8);
+ }
+ if (stream.getdents) stream.getdents = null;
+ try {
+ if (stream.stream_ops.close) {
+ stream.stream_ops.close(stream);
+ }
+ } catch (e) {
+ throw e;
+ } finally {
+ FS.closeStream(stream.fd);
+ }
+ stream.fd = null;
+ },
+ isClosed: stream => {
+ return stream.fd === null;
+ },
+ llseek: (stream, offset, whence) => {
+ if (FS.isClosed(stream)) {
+ throw new FS.ErrnoError(8);
+ }
+ if (!stream.seekable || !stream.stream_ops.llseek) {
+ throw new FS.ErrnoError(70);
+ }
+ if (whence != 0 && whence != 1 && whence != 2) {
+ throw new FS.ErrnoError(28);
+ }
+ stream.position = stream.stream_ops.llseek(stream, offset, whence);
+ stream.ungotten = [];
+ return stream.position;
+ },
+ read: (stream, buffer, offset, length, position) => {
+ if (length < 0 || position < 0) {
+ throw new FS.ErrnoError(28);
+ }
+ if (FS.isClosed(stream)) {
+ throw new FS.ErrnoError(8);
+ }
+ if ((stream.flags & 2097155) === 1) {
+ throw new FS.ErrnoError(8);
+ }
+ if (FS.isDir(stream.node.mode)) {
+ throw new FS.ErrnoError(31);
+ }
+ if (!stream.stream_ops.read) {
+ throw new FS.ErrnoError(28);
+ }
+ var seeking = typeof position != "undefined";
+ if (!seeking) {
+ position = stream.position;
+ } else if (!stream.seekable) {
+ throw new FS.ErrnoError(70);
+ }
+ var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position);
+ if (!seeking) stream.position += bytesRead;
+ return bytesRead;
+ },
+ write: (stream, buffer, offset, length, position, canOwn) => {
+ if (length < 0 || position < 0) {
+ throw new FS.ErrnoError(28);
+ }
+ if (FS.isClosed(stream)) {
+ throw new FS.ErrnoError(8);
+ }
+ if ((stream.flags & 2097155) === 0) {
+ throw new FS.ErrnoError(8);
+ }
+ if (FS.isDir(stream.node.mode)) {
+ throw new FS.ErrnoError(31);
+ }
+ if (!stream.stream_ops.write) {
+ throw new FS.ErrnoError(28);
+ }
+ if (stream.seekable && stream.flags & 1024) {
+ FS.llseek(stream, 0, 2);
+ }
+ var seeking = typeof position != "undefined";
+ if (!seeking) {
+ position = stream.position;
+ } else if (!stream.seekable) {
+ throw new FS.ErrnoError(70);
+ }
+ var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn);
+ if (!seeking) stream.position += bytesWritten;
+ return bytesWritten;
+ },
+ allocate: (stream, offset, length) => {
+ if (FS.isClosed(stream)) {
+ throw new FS.ErrnoError(8);
+ }
+ if (offset < 0 || length <= 0) {
+ throw new FS.ErrnoError(28);
+ }
+ if ((stream.flags & 2097155) === 0) {
+ throw new FS.ErrnoError(8);
+ }
+ if (!FS.isFile(stream.node.mode) && !FS.isDir(stream.node.mode)) {
+ throw new FS.ErrnoError(43);
+ }
+ if (!stream.stream_ops.allocate) {
+ throw new FS.ErrnoError(138);
+ }
+ stream.stream_ops.allocate(stream, offset, length);
+ },
+ mmap: (stream, address, length, position, prot, flags) => {
+ if ((prot & 2) !== 0 && (flags & 2) === 0 && (stream.flags & 2097155) !== 2) {
+ throw new FS.ErrnoError(2);
+ }
+ if ((stream.flags & 2097155) === 1) {
+ throw new FS.ErrnoError(2);
+ }
+ if (!stream.stream_ops.mmap) {
+ throw new FS.ErrnoError(43);
+ }
+ return stream.stream_ops.mmap(stream, address, length, position, prot, flags);
+ },
+ msync: (stream, buffer, offset, length, mmapFlags) => {
+ if (!stream || !stream.stream_ops.msync) {
+ return 0;
+ }
+ return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags);
+ },
+ munmap: stream => 0,
+ ioctl: (stream, cmd, arg) => {
+ if (!stream.stream_ops.ioctl) {
+ throw new FS.ErrnoError(59);
+ }
+ return stream.stream_ops.ioctl(stream, cmd, arg);
+ },
+ readFile: (path, opts = {}) => {
+ opts.flags = opts.flags || 0;
+ opts.encoding = opts.encoding || "binary";
+ if (opts.encoding !== "utf8" && opts.encoding !== "binary") {
+ throw new Error('Invalid encoding type "' + opts.encoding + '"');
+ }
+ var ret;
+ var stream = FS.open(path, opts.flags);
+ var stat = FS.stat(path);
+ var length = stat.size;
+ var buf = new Uint8Array(length);
+ FS.read(stream, buf, 0, length, 0);
+ if (opts.encoding === "utf8") {
+ ret = UTF8ArrayToString(buf, 0);
+ } else if (opts.encoding === "binary") {
+ ret = buf;
+ }
+ FS.close(stream);
+ return ret;
+ },
+ writeFile: (path, data, opts = {}) => {
+ opts.flags = opts.flags || 577;
+ var stream = FS.open(path, opts.flags, opts.mode);
+ if (typeof data == "string") {
+ var buf = new Uint8Array(lengthBytesUTF8(data) + 1);
+ var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length);
+ FS.write(stream, buf, 0, actualNumBytes, undefined, opts.canOwn);
+ } else if (ArrayBuffer.isView(data)) {
+ FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn);
+ } else {
+ throw new Error("Unsupported data type");
+ }
+ FS.close(stream);
+ },
+ cwd: () => FS.currentPath,
+ chdir: path => {
+ var lookup = FS.lookupPath(path, {
+ follow: true
+ });
+ if (lookup.node === null) {
+ throw new FS.ErrnoError(44);
+ }
+ if (!FS.isDir(lookup.node.mode)) {
+ throw new FS.ErrnoError(54);
+ }
+ var errCode = FS.nodePermissions(lookup.node, "x");
+ if (errCode) {
+ throw new FS.ErrnoError(errCode);
+ }
+ FS.currentPath = lookup.path;
+ },
+ createDefaultDirectories: () => {
+ FS.mkdir("/tmp");
+ FS.mkdir("/home");
+ FS.mkdir("/home/web_user");
+ },
+ createDefaultDevices: () => {
+ FS.mkdir("/dev");
+ FS.registerDevice(FS.makedev(1, 3), {
+ read: () => 0,
+ write: (stream, buffer, offset, length, pos) => length
+ });
+ FS.mkdev("/dev/null", FS.makedev(1, 3));
+ TTY.register(FS.makedev(5, 0), TTY.default_tty_ops);
+ TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops);
+ FS.mkdev("/dev/tty", FS.makedev(5, 0));
+ FS.mkdev("/dev/tty1", FS.makedev(6, 0));
+ var random_device = getRandomDevice();
+ FS.createDevice("/dev", "random", random_device);
+ FS.createDevice("/dev", "urandom", random_device);
+ FS.mkdir("/dev/shm");
+ FS.mkdir("/dev/shm/tmp");
+ },
+ createSpecialDirectories: () => {
+ FS.mkdir("/proc");
+ var proc_self = FS.mkdir("/proc/self");
+ FS.mkdir("/proc/self/fd");
+ FS.mount({
+ mount: () => {
+ var node = FS.createNode(proc_self, "fd", 16384 | 511, 73);
+ node.node_ops = {
+ lookup: (parent, name) => {
+ var fd = +name;
+ var stream = FS.getStream(fd);
+ if (!stream) throw new FS.ErrnoError(8);
+ var ret = {
+ parent: null,
+ mount: {
+ mountpoint: "fake"
+ },
+ node_ops: {
+ readlink: () => stream.path
+ }
+ };
+ ret.parent = ret;
+ return ret;
+ }
+ };
+ return node;
+ }
+ }, {}, "/proc/self/fd");
+ },
+ createStandardStreams: () => {
+ if (Module["stdin"]) {
+ FS.createDevice("/dev", "stdin", Module["stdin"]);
+ } else {
+ FS.symlink("/dev/tty", "/dev/stdin");
+ }
+ if (Module["stdout"]) {
+ FS.createDevice("/dev", "stdout", null, Module["stdout"]);
+ } else {
+ FS.symlink("/dev/tty", "/dev/stdout");
+ }
+ if (Module["stderr"]) {
+ FS.createDevice("/dev", "stderr", null, Module["stderr"]);
+ } else {
+ FS.symlink("/dev/tty1", "/dev/stderr");
+ }
+ var stdin = FS.open("/dev/stdin", 0);
+ var stdout = FS.open("/dev/stdout", 1);
+ var stderr = FS.open("/dev/stderr", 1);
+ assert(stdin.fd === 0, "invalid handle for stdin (" + stdin.fd + ")");
+ assert(stdout.fd === 1, "invalid handle for stdout (" + stdout.fd + ")");
+ assert(stderr.fd === 2, "invalid handle for stderr (" + stderr.fd + ")");
+ },
+ ensureErrnoError: () => {
+ if (FS.ErrnoError) return;
+ FS.ErrnoError = function ErrnoError(errno, node) {
+ this.node = node;
+ this.setErrno = function(errno) {
+ this.errno = errno;
+ for (var key in ERRNO_CODES) {
+ if (ERRNO_CODES[key] === errno) {
+ this.code = key;
+ break;
+ }
+ }
+ };
+ this.setErrno(errno);
+ this.message = ERRNO_MESSAGES[errno];
+ if (this.stack) {
+ Object.defineProperty(this, "stack", {
+ value: new Error().stack,
+ writable: true
+ });
+ this.stack = demangleAll(this.stack);
+ }
+ };
+ FS.ErrnoError.prototype = new Error();
+ FS.ErrnoError.prototype.constructor = FS.ErrnoError;
+ [ 44 ].forEach(code => {
+ FS.genericErrors[code] = new FS.ErrnoError(code);
+ FS.genericErrors[code].stack = "";
+ });
+ },
+ staticInit: () => {
+ FS.ensureErrnoError();
+ FS.nameTable = new Array(4096);
+ FS.mount(MEMFS, {}, "/");
+ FS.createDefaultDirectories();
+ FS.createDefaultDevices();
+ FS.createSpecialDirectories();
+ FS.filesystems = {
+ "MEMFS": MEMFS,
+ "IDBFS": IDBFS
+ };
+ },
+ init: (input, output, error) => {
+ assert(!FS.init.initialized, "FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)");
+ FS.init.initialized = true;
+ FS.ensureErrnoError();
+ Module["stdin"] = input || Module["stdin"];
+ Module["stdout"] = output || Module["stdout"];
+ Module["stderr"] = error || Module["stderr"];
+ FS.createStandardStreams();
+ },
+ quit: () => {
+ FS.init.initialized = false;
+ ___stdio_exit();
+ for (var i = 0; i < FS.streams.length; i++) {
+ var stream = FS.streams[i];
+ if (!stream) {
+ continue;
+ }
+ FS.close(stream);
+ }
+ },
+ getMode: (canRead, canWrite) => {
+ var mode = 0;
+ if (canRead) mode |= 292 | 73;
+ if (canWrite) mode |= 146;
+ return mode;
+ },
+ findObject: (path, dontResolveLastLink) => {
+ var ret = FS.analyzePath(path, dontResolveLastLink);
+ if (ret.exists) {
+ return ret.object;
+ } else {
+ return null;
+ }
+ },
+ analyzePath: (path, dontResolveLastLink) => {
+ try {
+ var lookup = FS.lookupPath(path, {
+ follow: !dontResolveLastLink
+ });
+ path = lookup.path;
+ } catch (e) {}
+ var ret = {
+ isRoot: false,
+ exists: false,
+ error: 0,
+ name: null,
+ path: null,
+ object: null,
+ parentExists: false,
+ parentPath: null,
+ parentObject: null
+ };
+ try {
+ var lookup = FS.lookupPath(path, {
+ parent: true
+ });
+ ret.parentExists = true;
+ ret.parentPath = lookup.path;
+ ret.parentObject = lookup.node;
+ ret.name = PATH.basename(path);
+ lookup = FS.lookupPath(path, {
+ follow: !dontResolveLastLink
+ });
+ ret.exists = true;
+ ret.path = lookup.path;
+ ret.object = lookup.node;
+ ret.name = lookup.node.name;
+ ret.isRoot = lookup.path === "/";
+ } catch (e) {
+ ret.error = e.errno;
+ }
+ return ret;
+ },
+ createPath: (parent, path, canRead, canWrite) => {
+ parent = typeof parent == "string" ? parent : FS.getPath(parent);
+ var parts = path.split("/").reverse();
+ while (parts.length) {
+ var part = parts.pop();
+ if (!part) continue;
+ var current = PATH.join2(parent, part);
+ try {
+ FS.mkdir(current);
+ } catch (e) {}
+ parent = current;
+ }
+ return current;
+ },
+ createFile: (parent, name, properties, canRead, canWrite) => {
+ var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name);
+ var mode = FS.getMode(canRead, canWrite);
+ return FS.create(path, mode);
+ },
+ createDataFile: (parent, name, data, canRead, canWrite, canOwn) => {
+ var path = name;
+ if (parent) {
+ parent = typeof parent == "string" ? parent : FS.getPath(parent);
+ path = name ? PATH.join2(parent, name) : parent;
+ }
+ var mode = FS.getMode(canRead, canWrite);
+ var node = FS.create(path, mode);
+ if (data) {
+ if (typeof data == "string") {
+ var arr = new Array(data.length);
+ for (var i = 0, len = data.length; i < len; ++i) arr[i] = data.charCodeAt(i);
+ data = arr;
+ }
+ FS.chmod(node, mode | 146);
+ var stream = FS.open(node, 577);
+ FS.write(stream, data, 0, data.length, 0, canOwn);
+ FS.close(stream);
+ FS.chmod(node, mode);
+ }
+ return node;
+ },
+ createDevice: (parent, name, input, output) => {
+ var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name);
+ var mode = FS.getMode(!!input, !!output);
+ if (!FS.createDevice.major) FS.createDevice.major = 64;
+ var dev = FS.makedev(FS.createDevice.major++, 0);
+ FS.registerDevice(dev, {
+ open: stream => {
+ stream.seekable = false;
+ },
+ close: stream => {
+ if (output && output.buffer && output.buffer.length) {
+ output(10);
+ }
+ },
+ read: (stream, buffer, offset, length, pos) => {
+ var bytesRead = 0;
+ for (var i = 0; i < length; i++) {
+ var result;
+ try {
+ result = input();
+ } catch (e) {
+ throw new FS.ErrnoError(29);
+ }
+ if (result === undefined && bytesRead === 0) {
+ throw new FS.ErrnoError(6);
+ }
+ if (result === null || result === undefined) break;
+ bytesRead++;
+ buffer[offset + i] = result;
+ }
+ if (bytesRead) {
+ stream.node.timestamp = Date.now();
+ }
+ return bytesRead;
+ },
+ write: (stream, buffer, offset, length, pos) => {
+ for (var i = 0; i < length; i++) {
+ try {
+ output(buffer[offset + i]);
+ } catch (e) {
+ throw new FS.ErrnoError(29);
+ }
+ }
+ if (length) {
+ stream.node.timestamp = Date.now();
+ }
+ return i;
+ }
+ });
+ return FS.mkdev(path, mode, dev);
+ },
+ forceLoadFile: obj => {
+ if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true;
+ if (typeof XMLHttpRequest != "undefined") {
+ throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.");
+ } else if (read_) {
+ try {
+ obj.contents = intArrayFromString(read_(obj.url), true);
+ obj.usedBytes = obj.contents.length;
+ } catch (e) {
+ throw new FS.ErrnoError(29);
+ }
+ } else {
+ throw new Error("Cannot load without read() or XMLHttpRequest.");
+ }
+ },
+ createLazyFile: (parent, name, url, canRead, canWrite) => {
+ function LazyUint8Array() {
+ this.lengthKnown = false;
+ this.chunks = [];
+ }
+ LazyUint8Array.prototype.get = function LazyUint8Array_get(idx) {
+ if (idx > this.length - 1 || idx < 0) {
+ return undefined;
+ }
+ var chunkOffset = idx % this.chunkSize;
+ var chunkNum = idx / this.chunkSize | 0;
+ return this.getter(chunkNum)[chunkOffset];
+ };
+ LazyUint8Array.prototype.setDataGetter = function LazyUint8Array_setDataGetter(getter) {
+ this.getter = getter;
+ };
+ LazyUint8Array.prototype.cacheLength = function LazyUint8Array_cacheLength() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("HEAD", url, false);
+ xhr.send(null);
+ if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status);
+ var datalength = Number(xhr.getResponseHeader("Content-length"));
+ var header;
+ var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes";
+ var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip";
+ var chunkSize = 1024 * 1024;
+ if (!hasByteServing) chunkSize = datalength;
+ var doXHR = (from, to) => {
+ if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!");
+ if (to > datalength - 1) throw new Error("only " + datalength + " bytes available! programmer error!");
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, false);
+ if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to);
+ xhr.responseType = "arraybuffer";
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType("text/plain; charset=x-user-defined");
+ }
+ xhr.send(null);
+ if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status);
+ if (xhr.response !== undefined) {
+ return new Uint8Array(xhr.response || []);
+ } else {
+ return intArrayFromString(xhr.responseText || "", true);
+ }
+ };
+ var lazyArray = this;
+ lazyArray.setDataGetter(chunkNum => {
+ var start = chunkNum * chunkSize;
+ var end = (chunkNum + 1) * chunkSize - 1;
+ end = Math.min(end, datalength - 1);
+ if (typeof lazyArray.chunks[chunkNum] == "undefined") {
+ lazyArray.chunks[chunkNum] = doXHR(start, end);
+ }
+ if (typeof lazyArray.chunks[chunkNum] == "undefined") throw new Error("doXHR failed!");
+ return lazyArray.chunks[chunkNum];
+ });
+ if (usesGzip || !datalength) {
+ chunkSize = datalength = 1;
+ datalength = this.getter(0).length;
+ chunkSize = datalength;
+ out("LazyFiles on gzip forces download of the whole file when length is accessed");
+ }
+ this._length = datalength;
+ this._chunkSize = chunkSize;
+ this.lengthKnown = true;
+ };
+ if (typeof XMLHttpRequest != "undefined") {
+ if (!ENVIRONMENT_IS_WORKER) throw "Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";
+ var lazyArray = new LazyUint8Array();
+ Object.defineProperties(lazyArray, {
+ length: {
+ get: function() {
+ if (!this.lengthKnown) {
+ this.cacheLength();
+ }
+ return this._length;
+ }
+ },
+ chunkSize: {
+ get: function() {
+ if (!this.lengthKnown) {
+ this.cacheLength();
+ }
+ return this._chunkSize;
+ }
+ }
+ });
+ var properties = {
+ isDevice: false,
+ contents: lazyArray
+ };
+ } else {
+ var properties = {
+ isDevice: false,
+ url: url
+ };
+ }
+ var node = FS.createFile(parent, name, properties, canRead, canWrite);
+ if (properties.contents) {
+ node.contents = properties.contents;
+ } else if (properties.url) {
+ node.contents = null;
+ node.url = properties.url;
+ }
+ Object.defineProperties(node, {
+ usedBytes: {
+ get: function() {
+ return this.contents.length;
+ }
+ }
+ });
+ var stream_ops = {};
+ var keys = Object.keys(node.stream_ops);
+ keys.forEach(key => {
+ var fn = node.stream_ops[key];
+ stream_ops[key] = function forceLoadLazyFile() {
+ FS.forceLoadFile(node);
+ return fn.apply(null, arguments);
+ };
+ });
+ stream_ops.read = ((stream, buffer, offset, length, position) => {
+ FS.forceLoadFile(node);
+ var contents = stream.node.contents;
+ if (position >= contents.length) return 0;
+ var size = Math.min(contents.length - position, length);
+ assert(size >= 0);
+ if (contents.slice) {
+ for (var i = 0; i < size; i++) {
+ buffer[offset + i] = contents[position + i];
+ }
+ } else {
+ for (var i = 0; i < size; i++) {
+ buffer[offset + i] = contents.get(position + i);
+ }
+ }
+ return size;
+ });
+ node.stream_ops = stream_ops;
+ return node;
+ },
+ createPreloadedFile: (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => {
+ var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent;
+ var dep = getUniqueRunDependency("cp " + fullname);
+ function processData(byteArray) {
+ function finish(byteArray) {
+ if (preFinish) preFinish();
+ if (!dontCreateFile) {
+ FS.createDataFile(parent, name, byteArray, canRead, canWrite, canOwn);
+ }
+ if (onload) onload();
+ removeRunDependency(dep);
+ }
+ if (Browser.handledByPreloadPlugin(byteArray, fullname, finish, () => {
+ if (onerror) onerror();
+ removeRunDependency(dep);
+ })) {
+ return;
+ }
+ finish(byteArray);
+ }
+ addRunDependency(dep);
+ if (typeof url == "string") {
+ asyncLoad(url, byteArray => processData(byteArray), onerror);
+ } else {
+ processData(url);
+ }
+ },
+ indexedDB: () => {
+ return window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
+ },
+ DB_NAME: () => {
+ return "EM_FS_" + window.location.pathname;
+ },
+ DB_VERSION: 20,
+ DB_STORE_NAME: "FILE_DATA",
+ saveFilesToDB: (paths, onload, onerror) => {
+ onload = onload || (() => {});
+ onerror = onerror || (() => {});
+ var indexedDB = FS.indexedDB();
+ try {
+ var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION);
+ } catch (e) {
+ return onerror(e);
+ }
+ openRequest.onupgradeneeded = (() => {
+ out("creating db");
+ var db = openRequest.result;
+ db.createObjectStore(FS.DB_STORE_NAME);
+ });
+ openRequest.onsuccess = (() => {
+ var db = openRequest.result;
+ var transaction = db.transaction([ FS.DB_STORE_NAME ], "readwrite");
+ var files = transaction.objectStore(FS.DB_STORE_NAME);
+ var ok = 0, fail = 0, total = paths.length;
+ function finish() {
+ if (fail == 0) onload(); else onerror();
+ }
+ paths.forEach(path => {
+ var putRequest = files.put(FS.analyzePath(path).object.contents, path);
+ putRequest.onsuccess = (() => {
+ ok++;
+ if (ok + fail == total) finish();
+ });
+ putRequest.onerror = (() => {
+ fail++;
+ if (ok + fail == total) finish();
+ });
+ });
+ transaction.onerror = onerror;
+ });
+ openRequest.onerror = onerror;
+ },
+ loadFilesFromDB: (paths, onload, onerror) => {
+ onload = onload || (() => {});
+ onerror = onerror || (() => {});
+ var indexedDB = FS.indexedDB();
+ try {
+ var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION);
+ } catch (e) {
+ return onerror(e);
+ }
+ openRequest.onupgradeneeded = onerror;
+ openRequest.onsuccess = (() => {
+ var db = openRequest.result;
+ try {
+ var transaction = db.transaction([ FS.DB_STORE_NAME ], "readonly");
+ } catch (e) {
+ onerror(e);
+ return;
+ }
+ var files = transaction.objectStore(FS.DB_STORE_NAME);
+ var ok = 0, fail = 0, total = paths.length;
+ function finish() {
+ if (fail == 0) onload(); else onerror();
+ }
+ paths.forEach(path => {
+ var getRequest = files.get(path);
+ getRequest.onsuccess = (() => {
+ if (FS.analyzePath(path).exists) {
+ FS.unlink(path);
+ }
+ FS.createDataFile(PATH.dirname(path), PATH.basename(path), getRequest.result, true, true, true);
+ ok++;
+ if (ok + fail == total) finish();
+ });
+ getRequest.onerror = (() => {
+ fail++;
+ if (ok + fail == total) finish();
+ });
+ });
+ transaction.onerror = onerror;
+ });
+ openRequest.onerror = onerror;
+ },
+ absolutePath: () => {
+ abort("FS.absolutePath has been removed; use PATH_FS.resolve instead");
+ },
+ createFolder: () => {
+ abort("FS.createFolder has been removed; use FS.mkdir instead");
+ },
+ createLink: () => {
+ abort("FS.createLink has been removed; use FS.symlink instead");
+ },
+ joinPath: () => {
+ abort("FS.joinPath has been removed; use PATH.join instead");
+ },
+ mmapAlloc: () => {
+ abort("FS.mmapAlloc has been replaced by the top level function mmapAlloc");
+ },
+ standardizePath: () => {
+ abort("FS.standardizePath has been removed; use PATH.normalize instead");
+ }
+};
+
+var SYSCALLS = {
+ DEFAULT_POLLMASK: 5,
+ calculateAt: function(dirfd, path, allowEmpty) {
+ if (path[0] === "/") {
+ return path;
+ }
+ var dir;
+ if (dirfd === -100) {
+ dir = FS.cwd();
+ } else {
+ var dirstream = FS.getStream(dirfd);
+ if (!dirstream) throw new FS.ErrnoError(8);
+ dir = dirstream.path;
+ }
+ if (path.length == 0) {
+ if (!allowEmpty) {
+ throw new FS.ErrnoError(44);
+ }
+ return dir;
+ }
+ return PATH.join2(dir, path);
+ },
+ doStat: function(func, path, buf) {
+ try {
+ var stat = func(path);
+ } catch (e) {
+ if (e && e.node && PATH.normalize(path) !== PATH.normalize(FS.getPath(e.node))) {
+ return -54;
+ }
+ throw e;
+ }
+ GROWABLE_HEAP_I32()[buf >> 2] = stat.dev;
+ GROWABLE_HEAP_I32()[buf + 4 >> 2] = 0;
+ GROWABLE_HEAP_I32()[buf + 8 >> 2] = stat.ino;
+ GROWABLE_HEAP_I32()[buf + 12 >> 2] = stat.mode;
+ GROWABLE_HEAP_I32()[buf + 16 >> 2] = stat.nlink;
+ GROWABLE_HEAP_I32()[buf + 20 >> 2] = stat.uid;
+ GROWABLE_HEAP_I32()[buf + 24 >> 2] = stat.gid;
+ GROWABLE_HEAP_I32()[buf + 28 >> 2] = stat.rdev;
+ GROWABLE_HEAP_I32()[buf + 32 >> 2] = 0;
+ tempI64 = [ stat.size >>> 0, (tempDouble = stat.size, +Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math.min(+Math.floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ],
+ GROWABLE_HEAP_I32()[buf + 40 >> 2] = tempI64[0], GROWABLE_HEAP_I32()[buf + 44 >> 2] = tempI64[1];
+ GROWABLE_HEAP_I32()[buf + 48 >> 2] = 4096;
+ GROWABLE_HEAP_I32()[buf + 52 >> 2] = stat.blocks;
+ GROWABLE_HEAP_I32()[buf + 56 >> 2] = stat.atime.getTime() / 1e3 | 0;
+ GROWABLE_HEAP_I32()[buf + 60 >> 2] = 0;
+ GROWABLE_HEAP_I32()[buf + 64 >> 2] = stat.mtime.getTime() / 1e3 | 0;
+ GROWABLE_HEAP_I32()[buf + 68 >> 2] = 0;
+ GROWABLE_HEAP_I32()[buf + 72 >> 2] = stat.ctime.getTime() / 1e3 | 0;
+ GROWABLE_HEAP_I32()[buf + 76 >> 2] = 0;
+ tempI64 = [ stat.ino >>> 0, (tempDouble = stat.ino, +Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math.min(+Math.floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ],
+ GROWABLE_HEAP_I32()[buf + 80 >> 2] = tempI64[0], GROWABLE_HEAP_I32()[buf + 84 >> 2] = tempI64[1];
+ return 0;
+ },
+ doMsync: function(addr, stream, len, flags, offset) {
+ var buffer = GROWABLE_HEAP_U8().slice(addr, addr + len);
+ FS.msync(stream, buffer, offset, len, flags);
+ },
+ doMkdir: function(path, mode) {
+ path = PATH.normalize(path);
+ if (path[path.length - 1] === "/") path = path.substr(0, path.length - 1);
+ FS.mkdir(path, mode, 0);
+ return 0;
+ },
+ doMknod: function(path, mode, dev) {
+ switch (mode & 61440) {
+ case 32768:
+ case 8192:
+ case 24576:
+ case 4096:
+ case 49152:
+ break;
+
+ default:
+ return -28;
+ }
+ FS.mknod(path, mode, dev);
+ return 0;
+ },
+ doReadlink: function(path, buf, bufsize) {
+ if (bufsize <= 0) return -28;
+ var ret = FS.readlink(path);
+ var len = Math.min(bufsize, lengthBytesUTF8(ret));
+ var endChar = GROWABLE_HEAP_I8()[buf + len];
+ stringToUTF8(ret, buf, bufsize + 1);
+ GROWABLE_HEAP_I8()[buf + len] = endChar;
+ return len;
+ },
+ doAccess: function(path, amode) {
+ if (amode & ~7) {
+ return -28;
+ }
+ var lookup = FS.lookupPath(path, {
+ follow: true
+ });
+ var node = lookup.node;
+ if (!node) {
+ return -44;
+ }
+ var perms = "";
+ if (amode & 4) perms += "r";
+ if (amode & 2) perms += "w";
+ if (amode & 1) perms += "x";
+ if (perms && FS.nodePermissions(node, perms)) {
+ return -2;
+ }
+ return 0;
+ },
+ doDup: function(path, flags, suggestFD) {
+ var suggest = FS.getStream(suggestFD);
+ if (suggest) FS.close(suggest);
+ return FS.open(path, flags, 0, suggestFD, suggestFD).fd;
+ },
+ doReadv: function(stream, iov, iovcnt, offset) {
+ var ret = 0;
+ for (var i = 0; i < iovcnt; i++) {
+ var ptr = GROWABLE_HEAP_I32()[iov + i * 8 >> 2];
+ var len = GROWABLE_HEAP_I32()[iov + (i * 8 + 4) >> 2];
+ var curr = FS.read(stream, GROWABLE_HEAP_I8(), ptr, len, offset);
+ if (curr < 0) return -1;
+ ret += curr;
+ if (curr < len) break;
+ }
+ return ret;
+ },
+ doWritev: function(stream, iov, iovcnt, offset) {
+ var ret = 0;
+ for (var i = 0; i < iovcnt; i++) {
+ var ptr = GROWABLE_HEAP_I32()[iov + i * 8 >> 2];
+ var len = GROWABLE_HEAP_I32()[iov + (i * 8 + 4) >> 2];
+ var curr = FS.write(stream, GROWABLE_HEAP_I8(), ptr, len, offset);
+ if (curr < 0) return -1;
+ ret += curr;
+ }
+ return ret;
+ },
+ varargs: undefined,
+ get: function() {
+ assert(SYSCALLS.varargs != undefined);
+ SYSCALLS.varargs += 4;
+ var ret = GROWABLE_HEAP_I32()[SYSCALLS.varargs - 4 >> 2];
+ return ret;
+ },
+ getStr: function(ptr) {
+ var ret = UTF8ToString(ptr);
+ return ret;
+ },
+ getStreamFromFD: function(fd) {
+ var stream = FS.getStream(fd);
+ if (!stream) throw new FS.ErrnoError(8);
+ return stream;
+ },
+ get64: function(low, high) {
+ if (low >= 0) assert(high === 0); else assert(high === -1);
+ return low;
+ }
+};
+
+function ___syscall__newselect(nfds, readfds, writefds, exceptfds, timeout) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(2, 1, nfds, readfds, writefds, exceptfds, timeout);
+ try {
+ assert(nfds <= 64, "nfds must be less than or equal to 64");
+ assert(!exceptfds, "exceptfds not supported");
+ var total = 0;
+ var srcReadLow = readfds ? GROWABLE_HEAP_I32()[readfds >> 2] : 0, srcReadHigh = readfds ? GROWABLE_HEAP_I32()[readfds + 4 >> 2] : 0;
+ var srcWriteLow = writefds ? GROWABLE_HEAP_I32()[writefds >> 2] : 0, srcWriteHigh = writefds ? GROWABLE_HEAP_I32()[writefds + 4 >> 2] : 0;
+ var srcExceptLow = exceptfds ? GROWABLE_HEAP_I32()[exceptfds >> 2] : 0, srcExceptHigh = exceptfds ? GROWABLE_HEAP_I32()[exceptfds + 4 >> 2] : 0;
+ var dstReadLow = 0, dstReadHigh = 0;
+ var dstWriteLow = 0, dstWriteHigh = 0;
+ var dstExceptLow = 0, dstExceptHigh = 0;
+ var allLow = (readfds ? GROWABLE_HEAP_I32()[readfds >> 2] : 0) | (writefds ? GROWABLE_HEAP_I32()[writefds >> 2] : 0) | (exceptfds ? GROWABLE_HEAP_I32()[exceptfds >> 2] : 0);
+ var allHigh = (readfds ? GROWABLE_HEAP_I32()[readfds + 4 >> 2] : 0) | (writefds ? GROWABLE_HEAP_I32()[writefds + 4 >> 2] : 0) | (exceptfds ? GROWABLE_HEAP_I32()[exceptfds + 4 >> 2] : 0);
+ var check = function(fd, low, high, val) {
+ return fd < 32 ? low & val : high & val;
+ };
+ for (var fd = 0; fd < nfds; fd++) {
+ var mask = 1 << fd % 32;
+ if (!check(fd, allLow, allHigh, mask)) {
+ continue;
+ }
+ var stream = FS.getStream(fd);
+ if (!stream) throw new FS.ErrnoError(8);
+ var flags = SYSCALLS.DEFAULT_POLLMASK;
+ if (stream.stream_ops.poll) {
+ flags = stream.stream_ops.poll(stream);
+ }
+ if (flags & 1 && check(fd, srcReadLow, srcReadHigh, mask)) {
+ fd < 32 ? dstReadLow = dstReadLow | mask : dstReadHigh = dstReadHigh | mask;
+ total++;
+ }
+ if (flags & 4 && check(fd, srcWriteLow, srcWriteHigh, mask)) {
+ fd < 32 ? dstWriteLow = dstWriteLow | mask : dstWriteHigh = dstWriteHigh | mask;
+ total++;
+ }
+ if (flags & 2 && check(fd, srcExceptLow, srcExceptHigh, mask)) {
+ fd < 32 ? dstExceptLow = dstExceptLow | mask : dstExceptHigh = dstExceptHigh | mask;
+ total++;
+ }
+ }
+ if (readfds) {
+ GROWABLE_HEAP_I32()[readfds >> 2] = dstReadLow;
+ GROWABLE_HEAP_I32()[readfds + 4 >> 2] = dstReadHigh;
+ }
+ if (writefds) {
+ GROWABLE_HEAP_I32()[writefds >> 2] = dstWriteLow;
+ GROWABLE_HEAP_I32()[writefds + 4 >> 2] = dstWriteHigh;
+ }
+ if (exceptfds) {
+ GROWABLE_HEAP_I32()[exceptfds >> 2] = dstExceptLow;
+ GROWABLE_HEAP_I32()[exceptfds + 4 >> 2] = dstExceptHigh;
+ }
+ return total;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+var SOCKFS = {
+ mount: function(mount) {
+ Module["websocket"] = Module["websocket"] && "object" === typeof Module["websocket"] ? Module["websocket"] : {};
+ Module["websocket"]._callbacks = {};
+ Module["websocket"]["on"] = function(event, callback) {
+ if ("function" === typeof callback) {
+ this._callbacks[event] = callback;
+ }
+ return this;
+ };
+ Module["websocket"].emit = function(event, param) {
+ if ("function" === typeof this._callbacks[event]) {
+ this._callbacks[event].call(this, param);
+ }
+ };
+ return FS.createNode(null, "/", 16384 | 511, 0);
+ },
+ createSocket: function(family, type, protocol) {
+ type &= ~526336;
+ var streaming = type == 1;
+ if (protocol) {
+ assert(streaming == (protocol == 6));
+ }
+ var sock = {
+ family: family,
+ type: type,
+ protocol: protocol,
+ server: null,
+ error: null,
+ peers: {},
+ pending: [],
+ recv_queue: [],
+ sock_ops: SOCKFS.websocket_sock_ops
+ };
+ var name = SOCKFS.nextname();
+ var node = FS.createNode(SOCKFS.root, name, 49152, 0);
+ node.sock = sock;
+ var stream = FS.createStream({
+ path: name,
+ node: node,
+ flags: 2,
+ seekable: false,
+ stream_ops: SOCKFS.stream_ops
+ });
+ sock.stream = stream;
+ return sock;
+ },
+ getSocket: function(fd) {
+ var stream = FS.getStream(fd);
+ if (!stream || !FS.isSocket(stream.node.mode)) {
+ return null;
+ }
+ return stream.node.sock;
+ },
+ stream_ops: {
+ poll: function(stream) {
+ var sock = stream.node.sock;
+ return sock.sock_ops.poll(sock);
+ },
+ ioctl: function(stream, request, varargs) {
+ var sock = stream.node.sock;
+ return sock.sock_ops.ioctl(sock, request, varargs);
+ },
+ read: function(stream, buffer, offset, length, position) {
+ var sock = stream.node.sock;
+ var msg = sock.sock_ops.recvmsg(sock, length);
+ if (!msg) {
+ return 0;
+ }
+ buffer.set(msg.buffer, offset);
+ return msg.buffer.length;
+ },
+ write: function(stream, buffer, offset, length, position) {
+ var sock = stream.node.sock;
+ return sock.sock_ops.sendmsg(sock, buffer, offset, length);
+ },
+ close: function(stream) {
+ var sock = stream.node.sock;
+ sock.sock_ops.close(sock);
+ }
+ },
+ nextname: function() {
+ if (!SOCKFS.nextname.current) {
+ SOCKFS.nextname.current = 0;
+ }
+ return "socket[" + SOCKFS.nextname.current++ + "]";
+ },
+ websocket_sock_ops: {
+ createPeer: function(sock, addr, port) {
+ var ws;
+ if (typeof addr == "object") {
+ ws = addr;
+ addr = null;
+ port = null;
+ }
+ if (ws) {
+ if (ws._socket) {
+ addr = ws._socket.remoteAddress;
+ port = ws._socket.remotePort;
+ } else {
+ var result = /ws[s]?:\/\/([^:]+):(\d+)/.exec(ws.url);
+ if (!result) {
+ throw new Error("WebSocket URL must be in the format ws(s)://address:port");
+ }
+ addr = result[1];
+ port = parseInt(result[2], 10);
+ }
+ } else {
+ try {
+ var runtimeConfig = Module["websocket"] && "object" === typeof Module["websocket"];
+ var url = "ws:#".replace("#", "//");
+ if (runtimeConfig) {
+ if ("string" === typeof Module["websocket"]["url"]) {
+ url = Module["websocket"]["url"];
+ }
+ }
+ if (url === "ws://" || url === "wss://") {
+ var parts = addr.split("/");
+ url = url + parts[0] + ":" + port + "/" + parts.slice(1).join("/");
+ }
+ var subProtocols = "binary";
+ if (runtimeConfig) {
+ if ("string" === typeof Module["websocket"]["subprotocol"]) {
+ subProtocols = Module["websocket"]["subprotocol"];
+ }
+ }
+ var opts = undefined;
+ if (subProtocols !== "null") {
+ subProtocols = subProtocols.replace(/^ +| +$/g, "").split(/ *, */);
+ opts = ENVIRONMENT_IS_NODE ? {
+ "protocol": subProtocols.toString()
+ } : subProtocols;
+ }
+ if (runtimeConfig && null === Module["websocket"]["subprotocol"]) {
+ subProtocols = "null";
+ opts = undefined;
+ }
+ var WebSocketConstructor;
+ {
+ WebSocketConstructor = WebSocket;
+ }
+ ws = new WebSocketConstructor(url, opts);
+ ws.binaryType = "arraybuffer";
+ } catch (e) {
+ throw new FS.ErrnoError(23);
+ }
+ }
+ var peer = {
+ addr: addr,
+ port: port,
+ socket: ws,
+ dgram_send_queue: []
+ };
+ SOCKFS.websocket_sock_ops.addPeer(sock, peer);
+ SOCKFS.websocket_sock_ops.handlePeerEvents(sock, peer);
+ if (sock.type === 2 && typeof sock.sport != "undefined") {
+ peer.dgram_send_queue.push(new Uint8Array([ 255, 255, 255, 255, "p".charCodeAt(0), "o".charCodeAt(0), "r".charCodeAt(0), "t".charCodeAt(0), (sock.sport & 65280) >> 8, sock.sport & 255 ]));
+ }
+ return peer;
+ },
+ getPeer: function(sock, addr, port) {
+ return sock.peers[addr + ":" + port];
+ },
+ addPeer: function(sock, peer) {
+ sock.peers[peer.addr + ":" + peer.port] = peer;
+ },
+ removePeer: function(sock, peer) {
+ delete sock.peers[peer.addr + ":" + peer.port];
+ },
+ handlePeerEvents: function(sock, peer) {
+ var first = true;
+ var handleOpen = function() {
+ Module["websocket"].emit("open", sock.stream.fd);
+ try {
+ var queued = peer.dgram_send_queue.shift();
+ while (queued) {
+ peer.socket.send(queued);
+ queued = peer.dgram_send_queue.shift();
+ }
+ } catch (e) {
+ peer.socket.close();
+ }
+ };
+ function handleMessage(data) {
+ if (typeof data == "string") {
+ var encoder = new TextEncoder();
+ data = encoder.encode(data);
+ } else {
+ assert(data.byteLength !== undefined);
+ if (data.byteLength == 0) {
+ return;
+ } else {
+ data = new Uint8Array(data);
+ }
+ }
+ var wasfirst = first;
+ first = false;
+ if (wasfirst && data.length === 10 && data[0] === 255 && data[1] === 255 && data[2] === 255 && data[3] === 255 && data[4] === "p".charCodeAt(0) && data[5] === "o".charCodeAt(0) && data[6] === "r".charCodeAt(0) && data[7] === "t".charCodeAt(0)) {
+ var newport = data[8] << 8 | data[9];
+ SOCKFS.websocket_sock_ops.removePeer(sock, peer);
+ peer.port = newport;
+ SOCKFS.websocket_sock_ops.addPeer(sock, peer);
+ return;
+ }
+ sock.recv_queue.push({
+ addr: peer.addr,
+ port: peer.port,
+ data: data
+ });
+ Module["websocket"].emit("message", sock.stream.fd);
+ }
+ if (ENVIRONMENT_IS_NODE) {
+ peer.socket.on("open", handleOpen);
+ peer.socket.on("message", function(data, flags) {
+ if (!flags.binary) {
+ return;
+ }
+ handleMessage(new Uint8Array(data).buffer);
+ });
+ peer.socket.on("close", function() {
+ Module["websocket"].emit("close", sock.stream.fd);
+ });
+ peer.socket.on("error", function(error) {
+ sock.error = 14;
+ Module["websocket"].emit("error", [ sock.stream.fd, sock.error, "ECONNREFUSED: Connection refused" ]);
+ });
+ } else {
+ peer.socket.onopen = handleOpen;
+ peer.socket.onclose = function() {
+ Module["websocket"].emit("close", sock.stream.fd);
+ };
+ peer.socket.onmessage = function peer_socket_onmessage(event) {
+ handleMessage(event.data);
+ };
+ peer.socket.onerror = function(error) {
+ sock.error = 14;
+ Module["websocket"].emit("error", [ sock.stream.fd, sock.error, "ECONNREFUSED: Connection refused" ]);
+ };
+ }
+ },
+ poll: function(sock) {
+ if (sock.type === 1 && sock.server) {
+ return sock.pending.length ? 64 | 1 : 0;
+ }
+ var mask = 0;
+ var dest = sock.type === 1 ? SOCKFS.websocket_sock_ops.getPeer(sock, sock.daddr, sock.dport) : null;
+ if (sock.recv_queue.length || !dest || dest && dest.socket.readyState === dest.socket.CLOSING || dest && dest.socket.readyState === dest.socket.CLOSED) {
+ mask |= 64 | 1;
+ }
+ if (!dest || dest && dest.socket.readyState === dest.socket.OPEN) {
+ mask |= 4;
+ }
+ if (dest && dest.socket.readyState === dest.socket.CLOSING || dest && dest.socket.readyState === dest.socket.CLOSED) {
+ mask |= 16;
+ }
+ return mask;
+ },
+ ioctl: function(sock, request, arg) {
+ switch (request) {
+ case 21531:
+ var bytes = 0;
+ if (sock.recv_queue.length) {
+ bytes = sock.recv_queue[0].data.length;
+ }
+ GROWABLE_HEAP_I32()[arg >> 2] = bytes;
+ return 0;
+
+ default:
+ return 28;
+ }
+ },
+ close: function(sock) {
+ if (sock.server) {
+ try {
+ sock.server.close();
+ } catch (e) {}
+ sock.server = null;
+ }
+ var peers = Object.keys(sock.peers);
+ for (var i = 0; i < peers.length; i++) {
+ var peer = sock.peers[peers[i]];
+ try {
+ peer.socket.close();
+ } catch (e) {}
+ SOCKFS.websocket_sock_ops.removePeer(sock, peer);
+ }
+ return 0;
+ },
+ bind: function(sock, addr, port) {
+ if (typeof sock.saddr != "undefined" || typeof sock.sport != "undefined") {
+ throw new FS.ErrnoError(28);
+ }
+ sock.saddr = addr;
+ sock.sport = port;
+ if (sock.type === 2) {
+ if (sock.server) {
+ sock.server.close();
+ sock.server = null;
+ }
+ try {
+ sock.sock_ops.listen(sock, 0);
+ } catch (e) {
+ if (!(e instanceof FS.ErrnoError)) throw e;
+ if (e.errno !== 138) throw e;
+ }
+ }
+ },
+ connect: function(sock, addr, port) {
+ if (sock.server) {
+ throw new FS.ErrnoError(138);
+ }
+ if (typeof sock.daddr != "undefined" && typeof sock.dport != "undefined") {
+ var dest = SOCKFS.websocket_sock_ops.getPeer(sock, sock.daddr, sock.dport);
+ if (dest) {
+ if (dest.socket.readyState === dest.socket.CONNECTING) {
+ throw new FS.ErrnoError(7);
+ } else {
+ throw new FS.ErrnoError(30);
+ }
+ }
+ }
+ var peer = SOCKFS.websocket_sock_ops.createPeer(sock, addr, port);
+ sock.daddr = peer.addr;
+ sock.dport = peer.port;
+ throw new FS.ErrnoError(26);
+ },
+ listen: function(sock, backlog) {
+ if (!ENVIRONMENT_IS_NODE) {
+ throw new FS.ErrnoError(138);
+ }
+ },
+ accept: function(listensock) {
+ if (!listensock.server) {
+ throw new FS.ErrnoError(28);
+ }
+ var newsock = listensock.pending.shift();
+ newsock.stream.flags = listensock.stream.flags;
+ return newsock;
+ },
+ getname: function(sock, peer) {
+ var addr, port;
+ if (peer) {
+ if (sock.daddr === undefined || sock.dport === undefined) {
+ throw new FS.ErrnoError(53);
+ }
+ addr = sock.daddr;
+ port = sock.dport;
+ } else {
+ addr = sock.saddr || 0;
+ port = sock.sport || 0;
+ }
+ return {
+ addr: addr,
+ port: port
+ };
+ },
+ sendmsg: function(sock, buffer, offset, length, addr, port) {
+ if (sock.type === 2) {
+ if (addr === undefined || port === undefined) {
+ addr = sock.daddr;
+ port = sock.dport;
+ }
+ if (addr === undefined || port === undefined) {
+ throw new FS.ErrnoError(17);
+ }
+ } else {
+ addr = sock.daddr;
+ port = sock.dport;
+ }
+ var dest = SOCKFS.websocket_sock_ops.getPeer(sock, addr, port);
+ if (sock.type === 1) {
+ if (!dest || dest.socket.readyState === dest.socket.CLOSING || dest.socket.readyState === dest.socket.CLOSED) {
+ throw new FS.ErrnoError(53);
+ } else if (dest.socket.readyState === dest.socket.CONNECTING) {
+ throw new FS.ErrnoError(6);
+ }
+ }
+ if (ArrayBuffer.isView(buffer)) {
+ offset += buffer.byteOffset;
+ buffer = buffer.buffer;
+ }
+ var data;
+ if (buffer instanceof SharedArrayBuffer) {
+ data = new Uint8Array(new Uint8Array(buffer.slice(offset, offset + length))).buffer;
+ } else {
+ data = buffer.slice(offset, offset + length);
+ }
+ if (sock.type === 2) {
+ if (!dest || dest.socket.readyState !== dest.socket.OPEN) {
+ if (!dest || dest.socket.readyState === dest.socket.CLOSING || dest.socket.readyState === dest.socket.CLOSED) {
+ dest = SOCKFS.websocket_sock_ops.createPeer(sock, addr, port);
+ }
+ dest.dgram_send_queue.push(data);
+ return length;
+ }
+ }
+ try {
+ dest.socket.send(data);
+ return length;
+ } catch (e) {
+ throw new FS.ErrnoError(28);
+ }
+ },
+ recvmsg: function(sock, length) {
+ if (sock.type === 1 && sock.server) {
+ throw new FS.ErrnoError(53);
+ }
+ var queued = sock.recv_queue.shift();
+ if (!queued) {
+ if (sock.type === 1) {
+ var dest = SOCKFS.websocket_sock_ops.getPeer(sock, sock.daddr, sock.dport);
+ if (!dest) {
+ throw new FS.ErrnoError(53);
+ } else if (dest.socket.readyState === dest.socket.CLOSING || dest.socket.readyState === dest.socket.CLOSED) {
+ return null;
+ } else {
+ throw new FS.ErrnoError(6);
+ }
+ } else {
+ throw new FS.ErrnoError(6);
+ }
+ }
+ var queuedLength = queued.data.byteLength || queued.data.length;
+ var queuedOffset = queued.data.byteOffset || 0;
+ var queuedBuffer = queued.data.buffer || queued.data;
+ var bytesRead = Math.min(length, queuedLength);
+ var res = {
+ buffer: new Uint8Array(queuedBuffer, queuedOffset, bytesRead),
+ addr: queued.addr,
+ port: queued.port
+ };
+ if (sock.type === 1 && bytesRead < queuedLength) {
+ var bytesRemaining = queuedLength - bytesRead;
+ queued.data = new Uint8Array(queuedBuffer, queuedOffset + bytesRead, bytesRemaining);
+ sock.recv_queue.unshift(queued);
+ }
+ return res;
+ }
+ }
+};
+
+function getSocketFromFD(fd) {
+ var socket = SOCKFS.getSocket(fd);
+ if (!socket) throw new FS.ErrnoError(8);
+ return socket;
+}
+
+var Sockets = {
+ BUFFER_SIZE: 10240,
+ MAX_BUFFER_SIZE: 10485760,
+ nextFd: 1,
+ fds: {},
+ nextport: 1,
+ maxport: 65535,
+ peer: null,
+ connections: {},
+ portmap: {},
+ localAddr: 4261412874,
+ addrPool: [ 33554442, 50331658, 67108874, 83886090, 100663306, 117440522, 134217738, 150994954, 167772170, 184549386, 201326602, 218103818, 234881034 ]
+};
+
+function inetPton4(str) {
+ var b = str.split(".");
+ for (var i = 0; i < 4; i++) {
+ var tmp = Number(b[i]);
+ if (isNaN(tmp)) return null;
+ b[i] = tmp;
+ }
+ return (b[0] | b[1] << 8 | b[2] << 16 | b[3] << 24) >>> 0;
+}
+
+function jstoi_q(str) {
+ return parseInt(str);
+}
+
+function inetPton6(str) {
+ var words;
+ var w, offset, z, i;
+ var valid6regx = /^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i;
+ var parts = [];
+ if (!valid6regx.test(str)) {
+ return null;
+ }
+ if (str === "::") {
+ return [ 0, 0, 0, 0, 0, 0, 0, 0 ];
+ }
+ if (str.startsWith("::")) {
+ str = str.replace("::", "Z:");
+ } else {
+ str = str.replace("::", ":Z:");
+ }
+ if (str.indexOf(".") > 0) {
+ str = str.replace(new RegExp("[.]", "g"), ":");
+ words = str.split(":");
+ words[words.length - 4] = jstoi_q(words[words.length - 4]) + jstoi_q(words[words.length - 3]) * 256;
+ words[words.length - 3] = jstoi_q(words[words.length - 2]) + jstoi_q(words[words.length - 1]) * 256;
+ words = words.slice(0, words.length - 2);
+ } else {
+ words = str.split(":");
+ }
+ offset = 0;
+ z = 0;
+ for (w = 0; w < words.length; w++) {
+ if (typeof words[w] == "string") {
+ if (words[w] === "Z") {
+ for (z = 0; z < 8 - words.length + 1; z++) {
+ parts[w + z] = 0;
+ }
+ offset = z - 1;
+ } else {
+ parts[w + offset] = _htons(parseInt(words[w], 16));
+ }
+ } else {
+ parts[w + offset] = words[w];
+ }
+ }
+ return [ parts[1] << 16 | parts[0], parts[3] << 16 | parts[2], parts[5] << 16 | parts[4], parts[7] << 16 | parts[6] ];
+}
+
+function writeSockaddr(sa, family, addr, port, addrlen) {
+ switch (family) {
+ case 2:
+ addr = inetPton4(addr);
+ zeroMemory(sa, 16);
+ if (addrlen) {
+ GROWABLE_HEAP_I32()[addrlen >> 2] = 16;
+ }
+ GROWABLE_HEAP_I16()[sa >> 1] = family;
+ GROWABLE_HEAP_I32()[sa + 4 >> 2] = addr;
+ GROWABLE_HEAP_I16()[sa + 2 >> 1] = _htons(port);
+ break;
+
+ case 10:
+ addr = inetPton6(addr);
+ zeroMemory(sa, 28);
+ if (addrlen) {
+ GROWABLE_HEAP_I32()[addrlen >> 2] = 28;
+ }
+ GROWABLE_HEAP_I32()[sa >> 2] = family;
+ GROWABLE_HEAP_I32()[sa + 8 >> 2] = addr[0];
+ GROWABLE_HEAP_I32()[sa + 12 >> 2] = addr[1];
+ GROWABLE_HEAP_I32()[sa + 16 >> 2] = addr[2];
+ GROWABLE_HEAP_I32()[sa + 20 >> 2] = addr[3];
+ GROWABLE_HEAP_I16()[sa + 2 >> 1] = _htons(port);
+ break;
+
+ default:
+ return 5;
+ }
+ return 0;
+}
+
+var DNS = {
+ address_map: {
+ id: 1,
+ addrs: {},
+ names: {}
+ },
+ lookup_name: function(name) {
+ var res = inetPton4(name);
+ if (res !== null) {
+ return name;
+ }
+ res = inetPton6(name);
+ if (res !== null) {
+ return name;
+ }
+ var addr;
+ if (DNS.address_map.addrs[name]) {
+ addr = DNS.address_map.addrs[name];
+ } else {
+ var id = DNS.address_map.id++;
+ assert(id < 65535, "exceeded max address mappings of 65535");
+ addr = "172.29." + (id & 255) + "." + (id & 65280);
+ DNS.address_map.names[addr] = name;
+ DNS.address_map.addrs[name] = addr;
+ }
+ return addr;
+ },
+ lookup_addr: function(addr) {
+ if (DNS.address_map.names[addr]) {
+ return DNS.address_map.names[addr];
+ }
+ return null;
+ }
+};
+
+function ___syscall_accept4(fd, addr, addrlen, flags) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(3, 1, fd, addr, addrlen, flags);
+ try {
+ var sock = getSocketFromFD(fd);
+ var newsock = sock.sock_ops.accept(sock);
+ if (addr) {
+ var errno = writeSockaddr(addr, newsock.family, DNS.lookup_name(newsock.daddr), newsock.dport, addrlen);
+ assert(!errno);
+ }
+ return newsock.stream.fd;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function inetNtop4(addr) {
+ return (addr & 255) + "." + (addr >> 8 & 255) + "." + (addr >> 16 & 255) + "." + (addr >> 24 & 255);
+}
+
+function inetNtop6(ints) {
+ var str = "";
+ var word = 0;
+ var longest = 0;
+ var lastzero = 0;
+ var zstart = 0;
+ var len = 0;
+ var i = 0;
+ var parts = [ ints[0] & 65535, ints[0] >> 16, ints[1] & 65535, ints[1] >> 16, ints[2] & 65535, ints[2] >> 16, ints[3] & 65535, ints[3] >> 16 ];
+ var hasipv4 = true;
+ var v4part = "";
+ for (i = 0; i < 5; i++) {
+ if (parts[i] !== 0) {
+ hasipv4 = false;
+ break;
+ }
+ }
+ if (hasipv4) {
+ v4part = inetNtop4(parts[6] | parts[7] << 16);
+ if (parts[5] === -1) {
+ str = "::ffff:";
+ str += v4part;
+ return str;
+ }
+ if (parts[5] === 0) {
+ str = "::";
+ if (v4part === "0.0.0.0") v4part = "";
+ if (v4part === "0.0.0.1") v4part = "1";
+ str += v4part;
+ return str;
+ }
+ }
+ for (word = 0; word < 8; word++) {
+ if (parts[word] === 0) {
+ if (word - lastzero > 1) {
+ len = 0;
+ }
+ lastzero = word;
+ len++;
+ }
+ if (len > longest) {
+ longest = len;
+ zstart = word - longest + 1;
+ }
+ }
+ for (word = 0; word < 8; word++) {
+ if (longest > 1) {
+ if (parts[word] === 0 && word >= zstart && word < zstart + longest) {
+ if (word === zstart) {
+ str += ":";
+ if (zstart === 0) str += ":";
+ }
+ continue;
+ }
+ }
+ str += Number(_ntohs(parts[word] & 65535)).toString(16);
+ str += word < 7 ? ":" : "";
+ }
+ return str;
+}
+
+function readSockaddr(sa, salen) {
+ var family = GROWABLE_HEAP_I16()[sa >> 1];
+ var port = _ntohs(GROWABLE_HEAP_U16()[sa + 2 >> 1]);
+ var addr;
+ switch (family) {
+ case 2:
+ if (salen !== 16) {
+ return {
+ errno: 28
+ };
+ }
+ addr = GROWABLE_HEAP_I32()[sa + 4 >> 2];
+ addr = inetNtop4(addr);
+ break;
+
+ case 10:
+ if (salen !== 28) {
+ return {
+ errno: 28
+ };
+ }
+ addr = [ GROWABLE_HEAP_I32()[sa + 8 >> 2], GROWABLE_HEAP_I32()[sa + 12 >> 2], GROWABLE_HEAP_I32()[sa + 16 >> 2], GROWABLE_HEAP_I32()[sa + 20 >> 2] ];
+ addr = inetNtop6(addr);
+ break;
+
+ default:
+ return {
+ errno: 5
+ };
+ }
+ return {
+ family: family,
+ addr: addr,
+ port: port
+ };
+}
+
+function getSocketAddress(addrp, addrlen, allowNull) {
+ if (allowNull && addrp === 0) return null;
+ var info = readSockaddr(addrp, addrlen);
+ if (info.errno) throw new FS.ErrnoError(info.errno);
+ info.addr = DNS.lookup_addr(info.addr) || info.addr;
+ return info;
+}
+
+function ___syscall_bind(fd, addr, addrlen) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(4, 1, fd, addr, addrlen);
+ try {
+ var sock = getSocketFromFD(fd);
+ var info = getSocketAddress(addr, addrlen);
+ sock.sock_ops.bind(sock, info.addr, info.port);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_chdir(path) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(5, 1, path);
+ try {
+ path = SYSCALLS.getStr(path);
+ FS.chdir(path);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_chmod(path, mode) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(6, 1, path, mode);
+ try {
+ path = SYSCALLS.getStr(path);
+ FS.chmod(path, mode);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_connect(fd, addr, addrlen) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(7, 1, fd, addr, addrlen);
+ try {
+ var sock = getSocketFromFD(fd);
+ var info = getSocketAddress(addr, addrlen);
+ sock.sock_ops.connect(sock, info.addr, info.port);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_faccessat(dirfd, path, amode, flags) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(8, 1, dirfd, path, amode, flags);
+ try {
+ path = SYSCALLS.getStr(path);
+ assert(flags === 0);
+ path = SYSCALLS.calculateAt(dirfd, path);
+ return SYSCALLS.doAccess(path, amode);
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_fchmod(fd, mode) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(9, 1, fd, mode);
+ try {
+ FS.fchmod(fd, mode);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_fcntl64(fd, cmd, varargs) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(10, 1, fd, cmd, varargs);
+ SYSCALLS.varargs = varargs;
+ try {
+ var stream = SYSCALLS.getStreamFromFD(fd);
+ switch (cmd) {
+ case 0:
+ {
+ var arg = SYSCALLS.get();
+ if (arg < 0) {
+ return -28;
+ }
+ var newStream;
+ newStream = FS.open(stream.path, stream.flags, 0, arg);
+ return newStream.fd;
+ }
+
+ case 1:
+ case 2:
+ return 0;
+
+ case 3:
+ return stream.flags;
+
+ case 4:
+ {
+ var arg = SYSCALLS.get();
+ stream.flags |= arg;
+ return 0;
+ }
+
+ case 5:
+ {
+ var arg = SYSCALLS.get();
+ var offset = 0;
+ GROWABLE_HEAP_I16()[arg + offset >> 1] = 2;
+ return 0;
+ }
+
+ case 6:
+ case 7:
+ return 0;
+
+ case 16:
+ case 8:
+ return -28;
+
+ case 9:
+ setErrNo(28);
+ return -1;
+
+ default:
+ {
+ return -28;
+ }
+ }
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_fstatat64(dirfd, path, buf, flags) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(11, 1, dirfd, path, buf, flags);
+ try {
+ path = SYSCALLS.getStr(path);
+ var nofollow = flags & 256;
+ var allowEmpty = flags & 4096;
+ flags = flags & ~4352;
+ assert(!flags, flags);
+ path = SYSCALLS.calculateAt(dirfd, path, allowEmpty);
+ return SYSCALLS.doStat(nofollow ? FS.lstat : FS.stat, path, buf);
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_getcwd(buf, size) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(12, 1, buf, size);
+ try {
+ if (size === 0) return -28;
+ var cwd = FS.cwd();
+ var cwdLengthInBytes = lengthBytesUTF8(cwd);
+ if (size < cwdLengthInBytes + 1) return -68;
+ stringToUTF8(cwd, buf, size);
+ return buf;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_getdents64(fd, dirp, count) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(13, 1, fd, dirp, count);
+ try {
+ var stream = SYSCALLS.getStreamFromFD(fd);
+ if (!stream.getdents) {
+ stream.getdents = FS.readdir(stream.path);
+ }
+ var struct_size = 280;
+ var pos = 0;
+ var off = FS.llseek(stream, 0, 1);
+ var idx = Math.floor(off / struct_size);
+ while (idx < stream.getdents.length && pos + struct_size <= count) {
+ var id;
+ var type;
+ var name = stream.getdents[idx];
+ if (name === ".") {
+ id = stream.node.id;
+ type = 4;
+ } else if (name === "..") {
+ var lookup = FS.lookupPath(stream.path, {
+ parent: true
+ });
+ id = lookup.node.id;
+ type = 4;
+ } else {
+ var child = FS.lookupNode(stream.node, name);
+ id = child.id;
+ type = FS.isChrdev(child.mode) ? 2 : FS.isDir(child.mode) ? 4 : FS.isLink(child.mode) ? 10 : 8;
+ }
+ assert(id);
+ tempI64 = [ id >>> 0, (tempDouble = id, +Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math.min(+Math.floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ],
+ GROWABLE_HEAP_I32()[dirp + pos >> 2] = tempI64[0], GROWABLE_HEAP_I32()[dirp + pos + 4 >> 2] = tempI64[1];
+ tempI64 = [ (idx + 1) * struct_size >>> 0, (tempDouble = (idx + 1) * struct_size,
+ +Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math.min(+Math.floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ],
+ GROWABLE_HEAP_I32()[dirp + pos + 8 >> 2] = tempI64[0], GROWABLE_HEAP_I32()[dirp + pos + 12 >> 2] = tempI64[1];
+ GROWABLE_HEAP_I16()[dirp + pos + 16 >> 1] = 280;
+ GROWABLE_HEAP_I8()[dirp + pos + 18 >> 0] = type;
+ stringToUTF8(name, dirp + pos + 19, 256);
+ pos += struct_size;
+ idx += 1;
+ }
+ FS.llseek(stream, idx * struct_size, 0);
+ return pos;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_getsockname(fd, addr, addrlen) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(14, 1, fd, addr, addrlen);
+ try {
+ err("__syscall_getsockname " + fd);
+ var sock = getSocketFromFD(fd);
+ var errno = writeSockaddr(addr, sock.family, DNS.lookup_name(sock.saddr || "0.0.0.0"), sock.sport, addrlen);
+ assert(!errno);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_getsockopt(fd, level, optname, optval, optlen) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(15, 1, fd, level, optname, optval, optlen);
+ try {
+ var sock = getSocketFromFD(fd);
+ if (level === 1) {
+ if (optname === 4) {
+ GROWABLE_HEAP_I32()[optval >> 2] = sock.error;
+ GROWABLE_HEAP_I32()[optlen >> 2] = 4;
+ sock.error = null;
+ return 0;
+ }
+ }
+ return -50;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_ioctl(fd, op, varargs) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(16, 1, fd, op, varargs);
+ SYSCALLS.varargs = varargs;
+ try {
+ var stream = SYSCALLS.getStreamFromFD(fd);
+ switch (op) {
+ case 21509:
+ case 21505:
+ {
+ if (!stream.tty) return -59;
+ return 0;
+ }
+
+ case 21510:
+ case 21511:
+ case 21512:
+ case 21506:
+ case 21507:
+ case 21508:
+ {
+ if (!stream.tty) return -59;
+ return 0;
+ }
+
+ case 21519:
+ {
+ if (!stream.tty) return -59;
+ var argp = SYSCALLS.get();
+ GROWABLE_HEAP_I32()[argp >> 2] = 0;
+ return 0;
+ }
+
+ case 21520:
+ {
+ if (!stream.tty) return -59;
+ return -28;
+ }
+
+ case 21531:
+ {
+ var argp = SYSCALLS.get();
+ return FS.ioctl(stream, op, argp);
+ }
+
+ case 21523:
+ {
+ if (!stream.tty) return -59;
+ return 0;
+ }
+
+ case 21524:
+ {
+ if (!stream.tty) return -59;
+ return 0;
+ }
+
+ default:
+ abort("bad ioctl syscall " + op);
+ }
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_listen(fd, backlog) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(17, 1, fd, backlog);
+ try {
+ var sock = getSocketFromFD(fd);
+ sock.sock_ops.listen(sock, backlog);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_lstat64(path, buf) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(18, 1, path, buf);
+ try {
+ path = SYSCALLS.getStr(path);
+ return SYSCALLS.doStat(FS.lstat, path, buf);
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_mkdir(path, mode) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(19, 1, path, mode);
+ try {
+ path = SYSCALLS.getStr(path);
+ return SYSCALLS.doMkdir(path, mode);
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_open(path, flags, varargs) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(20, 1, path, flags, varargs);
+ SYSCALLS.varargs = varargs;
+ try {
+ var pathname = SYSCALLS.getStr(path);
+ var mode = varargs ? SYSCALLS.get() : 0;
+ var stream = FS.open(pathname, flags, mode);
+ return stream.fd;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_poll(fds, nfds, timeout) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(21, 1, fds, nfds, timeout);
+ try {
+ var nonzero = 0;
+ for (var i = 0; i < nfds; i++) {
+ var pollfd = fds + 8 * i;
+ var fd = GROWABLE_HEAP_I32()[pollfd >> 2];
+ var events = GROWABLE_HEAP_I16()[pollfd + 4 >> 1];
+ var mask = 32;
+ var stream = FS.getStream(fd);
+ if (stream) {
+ mask = SYSCALLS.DEFAULT_POLLMASK;
+ if (stream.stream_ops.poll) {
+ mask = stream.stream_ops.poll(stream);
+ }
+ }
+ mask &= events | 8 | 16;
+ if (mask) nonzero++;
+ GROWABLE_HEAP_I16()[pollfd + 6 >> 1] = mask;
+ }
+ return nonzero;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_readlink(path, buf, bufsize) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(22, 1, path, buf, bufsize);
+ try {
+ path = SYSCALLS.getStr(path);
+ return SYSCALLS.doReadlink(path, buf, bufsize);
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_recvfrom(fd, buf, len, flags, addr, addrlen) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(23, 1, fd, buf, len, flags, addr, addrlen);
+ try {
+ var sock = getSocketFromFD(fd);
+ var msg = sock.sock_ops.recvmsg(sock, len);
+ if (!msg) return 0;
+ if (addr) {
+ var errno = writeSockaddr(addr, sock.family, DNS.lookup_name(msg.addr), msg.port, addrlen);
+ assert(!errno);
+ }
+ GROWABLE_HEAP_U8().set(msg.buffer, buf);
+ return msg.buffer.byteLength;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_rename(old_path, new_path) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(24, 1, old_path, new_path);
+ try {
+ old_path = SYSCALLS.getStr(old_path);
+ new_path = SYSCALLS.getStr(new_path);
+ FS.rename(old_path, new_path);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_rmdir(path) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(25, 1, path);
+ try {
+ path = SYSCALLS.getStr(path);
+ FS.rmdir(path);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_sendto(fd, message, length, flags, addr, addr_len) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(26, 1, fd, message, length, flags, addr, addr_len);
+ try {
+ var sock = getSocketFromFD(fd);
+ var dest = getSocketAddress(addr, addr_len, true);
+ if (!dest) {
+ return FS.write(sock.stream, GROWABLE_HEAP_I8(), message, length);
+ } else {
+ return sock.sock_ops.sendmsg(sock, GROWABLE_HEAP_I8(), message, length, dest.addr, dest.port);
+ }
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_socket(domain, type, protocol) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(27, 1, domain, type, protocol);
+ try {
+ var sock = SOCKFS.createSocket(domain, type, protocol);
+ assert(sock.stream.fd < 64);
+ return sock.stream.fd;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_stat64(path, buf) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(28, 1, path, buf);
+ try {
+ path = SYSCALLS.getStr(path);
+ return SYSCALLS.doStat(FS.stat, path, buf);
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_statfs64(path, size, buf) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(29, 1, path, size, buf);
+ try {
+ path = SYSCALLS.getStr(path);
+ assert(size === 64);
+ GROWABLE_HEAP_I32()[buf + 4 >> 2] = 4096;
+ GROWABLE_HEAP_I32()[buf + 40 >> 2] = 4096;
+ GROWABLE_HEAP_I32()[buf + 8 >> 2] = 1e6;
+ GROWABLE_HEAP_I32()[buf + 12 >> 2] = 5e5;
+ GROWABLE_HEAP_I32()[buf + 16 >> 2] = 5e5;
+ GROWABLE_HEAP_I32()[buf + 20 >> 2] = FS.nextInode;
+ GROWABLE_HEAP_I32()[buf + 24 >> 2] = 1e6;
+ GROWABLE_HEAP_I32()[buf + 28 >> 2] = 42;
+ GROWABLE_HEAP_I32()[buf + 44 >> 2] = 2;
+ GROWABLE_HEAP_I32()[buf + 36 >> 2] = 255;
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_symlink(target, linkpath) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(30, 1, target, linkpath);
+ try {
+ target = SYSCALLS.getStr(target);
+ linkpath = SYSCALLS.getStr(linkpath);
+ FS.symlink(target, linkpath);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function ___syscall_unlink(path) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(31, 1, path);
+ try {
+ path = SYSCALLS.getStr(path);
+ FS.unlink(path);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return -e.errno;
+ }
+}
+
+function __dlopen_js(filename, flag) {
+ abort("To use dlopen, you need to use Emscripten's linking support, see https://github.com/emscripten-core/emscripten/wiki/Linking");
+}
+
+function __dlsym_js(handle, symbol) {
+ abort("To use dlopen, you need to use Emscripten's linking support, see https://github.com/emscripten-core/emscripten/wiki/Linking");
+}
+
+function __emscripten_default_pthread_stack_size() {
+ return 2097152;
+}
+
+function __emscripten_notify_thread_queue(targetThreadId, mainThreadId) {
+ if (targetThreadId == mainThreadId) {
+ postMessage({
+ "cmd": "processQueuedMainThreadWork"
+ });
+ } else if (ENVIRONMENT_IS_PTHREAD) {
+ postMessage({
+ "targetThread": targetThreadId,
+ "cmd": "processThreadQueue"
+ });
+ } else {
+ var pthread = PThread.pthreads[targetThreadId];
+ var worker = pthread && pthread.worker;
+ if (!worker) {
+ err("Cannot send message to thread with ID " + targetThreadId + ", unknown thread ID!");
+ return;
+ }
+ worker.postMessage({
+ "cmd": "processThreadQueue"
+ });
+ }
+ return 1;
+}
+
+function __emscripten_proxied_gl_context_activated_from_main_browser_thread(contextHandle) {
+ GLctx = Module.ctx = GL.currentContext = contextHandle;
+ GL.currentContextIsProxied = true;
+}
+
+function __emscripten_throw_longjmp() {
+ throw "longjmp";
+}
+
+function __gmtime_js(time, tmPtr) {
+ var date = new Date(GROWABLE_HEAP_I32()[time >> 2] * 1e3);
+ GROWABLE_HEAP_I32()[tmPtr >> 2] = date.getUTCSeconds();
+ GROWABLE_HEAP_I32()[tmPtr + 4 >> 2] = date.getUTCMinutes();
+ GROWABLE_HEAP_I32()[tmPtr + 8 >> 2] = date.getUTCHours();
+ GROWABLE_HEAP_I32()[tmPtr + 12 >> 2] = date.getUTCDate();
+ GROWABLE_HEAP_I32()[tmPtr + 16 >> 2] = date.getUTCMonth();
+ GROWABLE_HEAP_I32()[tmPtr + 20 >> 2] = date.getUTCFullYear() - 1900;
+ GROWABLE_HEAP_I32()[tmPtr + 24 >> 2] = date.getUTCDay();
+ var start = Date.UTC(date.getUTCFullYear(), 0, 1, 0, 0, 0, 0);
+ var yday = (date.getTime() - start) / (1e3 * 60 * 60 * 24) | 0;
+ GROWABLE_HEAP_I32()[tmPtr + 28 >> 2] = yday;
+}
+
+function __localtime_js(time, tmPtr) {
+ var date = new Date(GROWABLE_HEAP_I32()[time >> 2] * 1e3);
+ GROWABLE_HEAP_I32()[tmPtr >> 2] = date.getSeconds();
+ GROWABLE_HEAP_I32()[tmPtr + 4 >> 2] = date.getMinutes();
+ GROWABLE_HEAP_I32()[tmPtr + 8 >> 2] = date.getHours();
+ GROWABLE_HEAP_I32()[tmPtr + 12 >> 2] = date.getDate();
+ GROWABLE_HEAP_I32()[tmPtr + 16 >> 2] = date.getMonth();
+ GROWABLE_HEAP_I32()[tmPtr + 20 >> 2] = date.getFullYear() - 1900;
+ GROWABLE_HEAP_I32()[tmPtr + 24 >> 2] = date.getDay();
+ var start = new Date(date.getFullYear(), 0, 1);
+ var yday = (date.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24) | 0;
+ GROWABLE_HEAP_I32()[tmPtr + 28 >> 2] = yday;
+ GROWABLE_HEAP_I32()[tmPtr + 36 >> 2] = -(date.getTimezoneOffset() * 60);
+ var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset();
+ var winterOffset = start.getTimezoneOffset();
+ var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset)) | 0;
+ GROWABLE_HEAP_I32()[tmPtr + 32 >> 2] = dst;
+}
+
+function _tzset_impl(timezone, daylight, tzname) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(32, 1, timezone, daylight, tzname);
+ var currentYear = new Date().getFullYear();
+ var winter = new Date(currentYear, 0, 1);
+ var summer = new Date(currentYear, 6, 1);
+ var winterOffset = winter.getTimezoneOffset();
+ var summerOffset = summer.getTimezoneOffset();
+ var stdTimezoneOffset = Math.max(winterOffset, summerOffset);
+ GROWABLE_HEAP_I32()[timezone >> 2] = stdTimezoneOffset * 60;
+ GROWABLE_HEAP_I32()[daylight >> 2] = Number(winterOffset != summerOffset);
+ function extractZone(date) {
+ var match = date.toTimeString().match(/\(([A-Za-z ]+)\)$/);
+ return match ? match[1] : "GMT";
+ }
+ var winterName = extractZone(winter);
+ var summerName = extractZone(summer);
+ var winterNamePtr = allocateUTF8(winterName);
+ var summerNamePtr = allocateUTF8(summerName);
+ if (summerOffset < winterOffset) {
+ GROWABLE_HEAP_I32()[tzname >> 2] = winterNamePtr;
+ GROWABLE_HEAP_I32()[tzname + 4 >> 2] = summerNamePtr;
+ } else {
+ GROWABLE_HEAP_I32()[tzname >> 2] = summerNamePtr;
+ GROWABLE_HEAP_I32()[tzname + 4 >> 2] = winterNamePtr;
+ }
+}
+
+function __tzset_js(timezone, daylight, tzname) {
+ if (__tzset_js.called) return;
+ __tzset_js.called = true;
+ _tzset_impl(timezone, daylight, tzname);
+}
+
+function _abort() {
+ abort("native code called abort()");
+}
+
+function _emscripten_set_main_loop_timing(mode, value) {
+ Browser.mainLoop.timingMode = mode;
+ Browser.mainLoop.timingValue = value;
+ if (!Browser.mainLoop.func) {
+ err("emscripten_set_main_loop_timing: Cannot set timing mode for main loop since a main loop does not exist! Call emscripten_set_main_loop first to set one up.");
+ return 1;
+ }
+ if (!Browser.mainLoop.running) {
+ runtimeKeepalivePush();
+ Browser.mainLoop.running = true;
+ }
+ if (mode == 0) {
+ Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_setTimeout() {
+ var timeUntilNextTick = Math.max(0, Browser.mainLoop.tickStartTime + value - _emscripten_get_now()) | 0;
+ setTimeout(Browser.mainLoop.runner, timeUntilNextTick);
+ };
+ Browser.mainLoop.method = "timeout";
+ } else if (mode == 1) {
+ Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_rAF() {
+ Browser.requestAnimationFrame(Browser.mainLoop.runner);
+ };
+ Browser.mainLoop.method = "rAF";
+ } else if (mode == 2) {
+ if (typeof setImmediate == "undefined") {
+ var setImmediates = [];
+ var emscriptenMainLoopMessageId = "setimmediate";
+ var Browser_setImmediate_messageHandler = function(event) {
+ if (event.data === emscriptenMainLoopMessageId || event.data.target === emscriptenMainLoopMessageId) {
+ event.stopPropagation();
+ setImmediates.shift()();
+ }
+ };
+ addEventListener("message", Browser_setImmediate_messageHandler, true);
+ setImmediate = function Browser_emulated_setImmediate(func) {
+ setImmediates.push(func);
+ if (ENVIRONMENT_IS_WORKER) {
+ if (Module["setImmediates"] === undefined) Module["setImmediates"] = [];
+ Module["setImmediates"].push(func);
+ postMessage({
+ target: emscriptenMainLoopMessageId
+ });
+ } else postMessage(emscriptenMainLoopMessageId, "*");
+ };
+ }
+ Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_setImmediate() {
+ setImmediate(Browser.mainLoop.runner);
+ };
+ Browser.mainLoop.method = "immediate";
+ }
+ return 0;
+}
+
+function runtimeKeepalivePush() {
+ runtimeKeepaliveCounter += 1;
+}
+
+function maybeExit() {
+ if (!keepRuntimeAlive()) {
+ try {
+ if (ENVIRONMENT_IS_PTHREAD) __emscripten_thread_exit(EXITSTATUS); else _exit(EXITSTATUS);
+ } catch (e) {
+ handleException(e);
+ }
+ }
+}
+
+function setMainLoop(browserIterationFunc, fps, simulateInfiniteLoop, arg, noSetTiming) {
+ assert(!Browser.mainLoop.func, "emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters.");
+ Browser.mainLoop.func = browserIterationFunc;
+ Browser.mainLoop.arg = arg;
+ var thisMainLoopId = Browser.mainLoop.currentlyRunningMainloop;
+ function checkIsRunning() {
+ if (thisMainLoopId < Browser.mainLoop.currentlyRunningMainloop) {
+ runtimeKeepalivePop();
+ maybeExit();
+ return false;
+ }
+ return true;
+ }
+ Browser.mainLoop.running = false;
+ Browser.mainLoop.runner = function Browser_mainLoop_runner() {
+ if (ABORT) return;
+ if (Browser.mainLoop.queue.length > 0) {
+ var start = Date.now();
+ var blocker = Browser.mainLoop.queue.shift();
+ blocker.func(blocker.arg);
+ if (Browser.mainLoop.remainingBlockers) {
+ var remaining = Browser.mainLoop.remainingBlockers;
+ var next = remaining % 1 == 0 ? remaining - 1 : Math.floor(remaining);
+ if (blocker.counted) {
+ Browser.mainLoop.remainingBlockers = next;
+ } else {
+ next = next + .5;
+ Browser.mainLoop.remainingBlockers = (8 * remaining + next) / 9;
+ }
+ }
+ out('main loop blocker "' + blocker.name + '" took ' + (Date.now() - start) + " ms");
+ Browser.mainLoop.updateStatus();
+ if (!checkIsRunning()) return;
+ setTimeout(Browser.mainLoop.runner, 0);
+ return;
+ }
+ if (!checkIsRunning()) return;
+ Browser.mainLoop.currentFrameNumber = Browser.mainLoop.currentFrameNumber + 1 | 0;
+ if (Browser.mainLoop.timingMode == 1 && Browser.mainLoop.timingValue > 1 && Browser.mainLoop.currentFrameNumber % Browser.mainLoop.timingValue != 0) {
+ Browser.mainLoop.scheduler();
+ return;
+ } else if (Browser.mainLoop.timingMode == 0) {
+ Browser.mainLoop.tickStartTime = _emscripten_get_now();
+ }
+ if (Browser.mainLoop.method === "timeout" && Module.ctx) {
+ warnOnce("Looks like you are rendering without using requestAnimationFrame for the main loop. You should use 0 for the frame rate in emscripten_set_main_loop in order to use requestAnimationFrame, as that can greatly improve your frame rates!");
+ Browser.mainLoop.method = "";
+ }
+ Browser.mainLoop.runIter(browserIterationFunc);
+ checkStackCookie();
+ if (!checkIsRunning()) return;
+ if (typeof SDL == "object" && SDL.audio && SDL.audio.queueNewAudioData) SDL.audio.queueNewAudioData();
+ Browser.mainLoop.scheduler();
+ };
+ if (!noSetTiming) {
+ if (fps && fps > 0) _emscripten_set_main_loop_timing(0, 1e3 / fps); else _emscripten_set_main_loop_timing(1, 1);
+ Browser.mainLoop.scheduler();
+ }
+ if (simulateInfiniteLoop) {
+ throw "unwind";
+ }
+}
+
+function callUserCallback(func, synchronous) {
+ if (runtimeExited || ABORT) {
+ err("user callback triggered after runtime exited or application aborted. Ignoring.");
+ return;
+ }
+ if (synchronous) {
+ func();
+ return;
+ }
+ try {
+ func();
+ maybeExit();
+ } catch (e) {
+ handleException(e);
+ }
+}
+
+function runtimeKeepalivePop() {
+ assert(runtimeKeepaliveCounter > 0);
+ runtimeKeepaliveCounter -= 1;
+}
+
+function safeSetTimeout(func, timeout) {
+ runtimeKeepalivePush();
+ return setTimeout(function() {
+ runtimeKeepalivePop();
+ callUserCallback(func);
+ }, timeout);
+}
+
+var Browser = {
+ mainLoop: {
+ running: false,
+ scheduler: null,
+ method: "",
+ currentlyRunningMainloop: 0,
+ func: null,
+ arg: 0,
+ timingMode: 0,
+ timingValue: 0,
+ currentFrameNumber: 0,
+ queue: [],
+ pause: function() {
+ Browser.mainLoop.scheduler = null;
+ Browser.mainLoop.currentlyRunningMainloop++;
+ },
+ resume: function() {
+ Browser.mainLoop.currentlyRunningMainloop++;
+ var timingMode = Browser.mainLoop.timingMode;
+ var timingValue = Browser.mainLoop.timingValue;
+ var func = Browser.mainLoop.func;
+ Browser.mainLoop.func = null;
+ setMainLoop(func, 0, false, Browser.mainLoop.arg, true);
+ _emscripten_set_main_loop_timing(timingMode, timingValue);
+ Browser.mainLoop.scheduler();
+ },
+ updateStatus: function() {
+ if (Module["setStatus"]) {
+ var message = Module["statusMessage"] || "Please wait...";
+ var remaining = Browser.mainLoop.remainingBlockers;
+ var expected = Browser.mainLoop.expectedBlockers;
+ if (remaining) {
+ if (remaining < expected) {
+ Module["setStatus"](message + " (" + (expected - remaining) + "/" + expected + ")");
+ } else {
+ Module["setStatus"](message);
+ }
+ } else {
+ Module["setStatus"]("");
+ }
+ }
+ },
+ runIter: function(func) {
+ if (ABORT) return;
+ if (Module["preMainLoop"]) {
+ var preRet = Module["preMainLoop"]();
+ if (preRet === false) {
+ return;
+ }
+ }
+ callUserCallback(func);
+ if (Module["postMainLoop"]) Module["postMainLoop"]();
+ }
+ },
+ isFullscreen: false,
+ pointerLock: false,
+ moduleContextCreatedCallbacks: [],
+ workers: [],
+ init: function() {
+ if (!Module["preloadPlugins"]) Module["preloadPlugins"] = [];
+ if (Browser.initted) return;
+ Browser.initted = true;
+ try {
+ new Blob();
+ Browser.hasBlobConstructor = true;
+ } catch (e) {
+ Browser.hasBlobConstructor = false;
+ out("warning: no blob constructor, cannot create blobs with mimetypes");
+ }
+ Browser.BlobBuilder = typeof MozBlobBuilder != "undefined" ? MozBlobBuilder : typeof WebKitBlobBuilder != "undefined" ? WebKitBlobBuilder : !Browser.hasBlobConstructor ? out("warning: no BlobBuilder") : null;
+ Browser.URLObject = typeof window != "undefined" ? window.URL ? window.URL : window.webkitURL : undefined;
+ if (!Module.noImageDecoding && typeof Browser.URLObject == "undefined") {
+ out("warning: Browser does not support creating object URLs. Built-in browser image decoding will not be available.");
+ Module.noImageDecoding = true;
+ }
+ var imagePlugin = {};
+ imagePlugin["canHandle"] = function imagePlugin_canHandle(name) {
+ return !Module.noImageDecoding && /\.(jpg|jpeg|png|bmp)$/i.test(name);
+ };
+ imagePlugin["handle"] = function imagePlugin_handle(byteArray, name, onload, onerror) {
+ var b = null;
+ if (Browser.hasBlobConstructor) {
+ try {
+ b = new Blob([ byteArray ], {
+ type: Browser.getMimetype(name)
+ });
+ if (b.size !== byteArray.length) {
+ b = new Blob([ new Uint8Array(byteArray).buffer ], {
+ type: Browser.getMimetype(name)
+ });
+ }
+ } catch (e) {
+ warnOnce("Blob constructor present but fails: " + e + "; falling back to blob builder");
+ }
+ }
+ if (!b) {
+ var bb = new Browser.BlobBuilder();
+ bb.append(new Uint8Array(byteArray).buffer);
+ b = bb.getBlob();
+ }
+ var url = Browser.URLObject.createObjectURL(b);
+ assert(typeof url == "string", "createObjectURL must return a url as a string");
+ var img = new Image();
+ img.onload = (() => {
+ assert(img.complete, "Image " + name + " could not be decoded");
+ var canvas = document.createElement("canvas");
+ canvas.width = img.width;
+ canvas.height = img.height;
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0);
+ Module["preloadedImages"][name] = canvas;
+ Browser.URLObject.revokeObjectURL(url);
+ if (onload) onload(byteArray);
+ });
+ img.onerror = (event => {
+ out("Image " + url + " could not be decoded");
+ if (onerror) onerror();
+ });
+ img.src = url;
+ };
+ Module["preloadPlugins"].push(imagePlugin);
+ var audioPlugin = {};
+ audioPlugin["canHandle"] = function audioPlugin_canHandle(name) {
+ return !Module.noAudioDecoding && name.substr(-4) in {
+ ".ogg": 1,
+ ".wav": 1,
+ ".mp3": 1
+ };
+ };
+ audioPlugin["handle"] = function audioPlugin_handle(byteArray, name, onload, onerror) {
+ var done = false;
+ function finish(audio) {
+ if (done) return;
+ done = true;
+ Module["preloadedAudios"][name] = audio;
+ if (onload) onload(byteArray);
+ }
+ function fail() {
+ if (done) return;
+ done = true;
+ Module["preloadedAudios"][name] = new Audio();
+ if (onerror) onerror();
+ }
+ if (Browser.hasBlobConstructor) {
+ try {
+ var b = new Blob([ byteArray ], {
+ type: Browser.getMimetype(name)
+ });
+ } catch (e) {
+ return fail();
+ }
+ var url = Browser.URLObject.createObjectURL(b);
+ assert(typeof url == "string", "createObjectURL must return a url as a string");
+ var audio = new Audio();
+ audio.addEventListener("canplaythrough", function() {
+ finish(audio);
+ }, false);
+ audio.onerror = function audio_onerror(event) {
+ if (done) return;
+ out("warning: browser could not fully decode audio " + name + ", trying slower base64 approach");
+ function encode64(data) {
+ var BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ var PAD = "=";
+ var ret = "";
+ var leftchar = 0;
+ var leftbits = 0;
+ for (var i = 0; i < data.length; i++) {
+ leftchar = leftchar << 8 | data[i];
+ leftbits += 8;
+ while (leftbits >= 6) {
+ var curr = leftchar >> leftbits - 6 & 63;
+ leftbits -= 6;
+ ret += BASE[curr];
+ }
+ }
+ if (leftbits == 2) {
+ ret += BASE[(leftchar & 3) << 4];
+ ret += PAD + PAD;
+ } else if (leftbits == 4) {
+ ret += BASE[(leftchar & 15) << 2];
+ ret += PAD;
+ }
+ return ret;
+ }
+ audio.src = "data:audio/x-" + name.substr(-3) + ";base64," + encode64(byteArray);
+ finish(audio);
+ };
+ audio.src = url;
+ safeSetTimeout(function() {
+ finish(audio);
+ }, 1e4);
+ } else {
+ return fail();
+ }
+ };
+ Module["preloadPlugins"].push(audioPlugin);
+ function pointerLockChange() {
+ Browser.pointerLock = document["pointerLockElement"] === Module["canvas"] || document["mozPointerLockElement"] === Module["canvas"] || document["webkitPointerLockElement"] === Module["canvas"] || document["msPointerLockElement"] === Module["canvas"];
+ }
+ var canvas = Module["canvas"];
+ if (canvas) {
+ canvas.requestPointerLock = canvas["requestPointerLock"] || canvas["mozRequestPointerLock"] || canvas["webkitRequestPointerLock"] || canvas["msRequestPointerLock"] || function() {};
+ canvas.exitPointerLock = document["exitPointerLock"] || document["mozExitPointerLock"] || document["webkitExitPointerLock"] || document["msExitPointerLock"] || function() {};
+ canvas.exitPointerLock = canvas.exitPointerLock.bind(document);
+ document.addEventListener("pointerlockchange", pointerLockChange, false);
+ document.addEventListener("mozpointerlockchange", pointerLockChange, false);
+ document.addEventListener("webkitpointerlockchange", pointerLockChange, false);
+ document.addEventListener("mspointerlockchange", pointerLockChange, false);
+ if (Module["elementPointerLock"]) {
+ canvas.addEventListener("click", function(ev) {
+ if (!Browser.pointerLock && Module["canvas"].requestPointerLock) {
+ Module["canvas"].requestPointerLock();
+ ev.preventDefault();
+ }
+ }, false);
+ }
+ }
+ },
+ handledByPreloadPlugin: function(byteArray, fullname, finish, onerror) {
+ Browser.init();
+ var handled = false;
+ Module["preloadPlugins"].forEach(function(plugin) {
+ if (handled) return;
+ if (plugin["canHandle"](fullname)) {
+ plugin["handle"](byteArray, fullname, finish, onerror);
+ handled = true;
+ }
+ });
+ return handled;
+ },
+ createContext: function(canvas, useWebGL, setInModule, webGLContextAttributes) {
+ if (useWebGL && Module.ctx && canvas == Module.canvas) return Module.ctx;
+ var ctx;
+ var contextHandle;
+ if (useWebGL) {
+ var contextAttributes = {
+ antialias: false,
+ alpha: false,
+ majorVersion: typeof WebGL2RenderingContext != "undefined" ? 2 : 1
+ };
+ if (webGLContextAttributes) {
+ for (var attribute in webGLContextAttributes) {
+ contextAttributes[attribute] = webGLContextAttributes[attribute];
+ }
+ }
+ if (typeof GL != "undefined") {
+ contextHandle = GL.createContext(canvas, contextAttributes);
+ if (contextHandle) {
+ ctx = GL.getContext(contextHandle).GLctx;
+ }
+ }
+ } else {
+ ctx = canvas.getContext("2d");
+ }
+ if (!ctx) return null;
+ if (setInModule) {
+ if (!useWebGL) assert(typeof GLctx == "undefined", "cannot set in module if GLctx is used, but we are a non-GL context that would replace it");
+ Module.ctx = ctx;
+ if (useWebGL) GL.makeContextCurrent(contextHandle);
+ Module.useWebGL = useWebGL;
+ Browser.moduleContextCreatedCallbacks.forEach(function(callback) {
+ callback();
+ });
+ Browser.init();
+ }
+ return ctx;
+ },
+ destroyContext: function(canvas, useWebGL, setInModule) {},
+ fullscreenHandlersInstalled: false,
+ lockPointer: undefined,
+ resizeCanvas: undefined,
+ requestFullscreen: function(lockPointer, resizeCanvas) {
+ Browser.lockPointer = lockPointer;
+ Browser.resizeCanvas = resizeCanvas;
+ if (typeof Browser.lockPointer == "undefined") Browser.lockPointer = true;
+ if (typeof Browser.resizeCanvas == "undefined") Browser.resizeCanvas = false;
+ var canvas = Module["canvas"];
+ function fullscreenChange() {
+ Browser.isFullscreen = false;
+ var canvasContainer = canvas.parentNode;
+ if ((document["fullscreenElement"] || document["mozFullScreenElement"] || document["msFullscreenElement"] || document["webkitFullscreenElement"] || document["webkitCurrentFullScreenElement"]) === canvasContainer) {
+ canvas.exitFullscreen = Browser.exitFullscreen;
+ if (Browser.lockPointer) canvas.requestPointerLock();
+ Browser.isFullscreen = true;
+ if (Browser.resizeCanvas) {
+ Browser.setFullscreenCanvasSize();
+ } else {
+ Browser.updateCanvasDimensions(canvas);
+ }
+ } else {
+ canvasContainer.parentNode.insertBefore(canvas, canvasContainer);
+ canvasContainer.parentNode.removeChild(canvasContainer);
+ if (Browser.resizeCanvas) {
+ Browser.setWindowedCanvasSize();
+ } else {
+ Browser.updateCanvasDimensions(canvas);
+ }
+ }
+ if (Module["onFullScreen"]) Module["onFullScreen"](Browser.isFullscreen);
+ if (Module["onFullscreen"]) Module["onFullscreen"](Browser.isFullscreen);
+ }
+ if (!Browser.fullscreenHandlersInstalled) {
+ Browser.fullscreenHandlersInstalled = true;
+ document.addEventListener("fullscreenchange", fullscreenChange, false);
+ document.addEventListener("mozfullscreenchange", fullscreenChange, false);
+ document.addEventListener("webkitfullscreenchange", fullscreenChange, false);
+ document.addEventListener("MSFullscreenChange", fullscreenChange, false);
+ }
+ var canvasContainer = document.createElement("div");
+ canvas.parentNode.insertBefore(canvasContainer, canvas);
+ canvasContainer.appendChild(canvas);
+ canvasContainer.requestFullscreen = canvasContainer["requestFullscreen"] || canvasContainer["mozRequestFullScreen"] || canvasContainer["msRequestFullscreen"] || (canvasContainer["webkitRequestFullscreen"] ? function() {
+ canvasContainer["webkitRequestFullscreen"](Element["ALLOW_KEYBOARD_INPUT"]);
+ } : null) || (canvasContainer["webkitRequestFullScreen"] ? function() {
+ canvasContainer["webkitRequestFullScreen"](Element["ALLOW_KEYBOARD_INPUT"]);
+ } : null);
+ canvasContainer.requestFullscreen();
+ },
+ requestFullScreen: function() {
+ abort("Module.requestFullScreen has been replaced by Module.requestFullscreen (without a capital S)");
+ },
+ exitFullscreen: function() {
+ if (!Browser.isFullscreen) {
+ return false;
+ }
+ var CFS = document["exitFullscreen"] || document["cancelFullScreen"] || document["mozCancelFullScreen"] || document["msExitFullscreen"] || document["webkitCancelFullScreen"] || function() {};
+ CFS.apply(document, []);
+ return true;
+ },
+ nextRAF: 0,
+ fakeRequestAnimationFrame: function(func) {
+ var now = Date.now();
+ if (Browser.nextRAF === 0) {
+ Browser.nextRAF = now + 1e3 / 60;
+ } else {
+ while (now + 2 >= Browser.nextRAF) {
+ Browser.nextRAF += 1e3 / 60;
+ }
+ }
+ var delay = Math.max(Browser.nextRAF - now, 0);
+ setTimeout(func, delay);
+ },
+ requestAnimationFrame: function(func) {
+ if (typeof requestAnimationFrame == "function") {
+ requestAnimationFrame(func);
+ return;
+ }
+ var RAF = Browser.fakeRequestAnimationFrame;
+ RAF(func);
+ },
+ safeSetTimeout: function(func) {
+ return safeSetTimeout(func);
+ },
+ safeRequestAnimationFrame: function(func) {
+ runtimeKeepalivePush();
+ return Browser.requestAnimationFrame(function() {
+ runtimeKeepalivePop();
+ callUserCallback(func);
+ });
+ },
+ getMimetype: function(name) {
+ return {
+ "jpg": "image/jpeg",
+ "jpeg": "image/jpeg",
+ "png": "image/png",
+ "bmp": "image/bmp",
+ "ogg": "audio/ogg",
+ "wav": "audio/wav",
+ "mp3": "audio/mpeg"
+ }[name.substr(name.lastIndexOf(".") + 1)];
+ },
+ getUserMedia: function(func) {
+ if (!window.getUserMedia) {
+ window.getUserMedia = navigator["getUserMedia"] || navigator["mozGetUserMedia"];
+ }
+ window.getUserMedia(func);
+ },
+ getMovementX: function(event) {
+ return event["movementX"] || event["mozMovementX"] || event["webkitMovementX"] || 0;
+ },
+ getMovementY: function(event) {
+ return event["movementY"] || event["mozMovementY"] || event["webkitMovementY"] || 0;
+ },
+ getMouseWheelDelta: function(event) {
+ var delta = 0;
+ switch (event.type) {
+ case "DOMMouseScroll":
+ delta = event.detail / 3;
+ break;
+
+ case "mousewheel":
+ delta = event.wheelDelta / 120;
+ break;
+
+ case "wheel":
+ delta = event.deltaY;
+ switch (event.deltaMode) {
+ case 0:
+ delta /= 100;
+ break;
+
+ case 1:
+ delta /= 3;
+ break;
+
+ case 2:
+ delta *= 80;
+ break;
+
+ default:
+ throw "unrecognized mouse wheel delta mode: " + event.deltaMode;
+ }
+ break;
+
+ default:
+ throw "unrecognized mouse wheel event: " + event.type;
+ }
+ return delta;
+ },
+ mouseX: 0,
+ mouseY: 0,
+ mouseMovementX: 0,
+ mouseMovementY: 0,
+ touches: {},
+ lastTouches: {},
+ calculateMouseEvent: function(event) {
+ if (Browser.pointerLock) {
+ if (event.type != "mousemove" && "mozMovementX" in event) {
+ Browser.mouseMovementX = Browser.mouseMovementY = 0;
+ } else {
+ Browser.mouseMovementX = Browser.getMovementX(event);
+ Browser.mouseMovementY = Browser.getMovementY(event);
+ }
+ if (typeof SDL != "undefined") {
+ Browser.mouseX = SDL.mouseX + Browser.mouseMovementX;
+ Browser.mouseY = SDL.mouseY + Browser.mouseMovementY;
+ } else {
+ Browser.mouseX += Browser.mouseMovementX;
+ Browser.mouseY += Browser.mouseMovementY;
+ }
+ } else {
+ var rect = Module["canvas"].getBoundingClientRect();
+ var cw = Module["canvas"].width;
+ var ch = Module["canvas"].height;
+ var scrollX = typeof window.scrollX != "undefined" ? window.scrollX : window.pageXOffset;
+ var scrollY = typeof window.scrollY != "undefined" ? window.scrollY : window.pageYOffset;
+ assert(typeof scrollX != "undefined" && typeof scrollY != "undefined", "Unable to retrieve scroll position, mouse positions likely broken.");
+ if (event.type === "touchstart" || event.type === "touchend" || event.type === "touchmove") {
+ var touch = event.touch;
+ if (touch === undefined) {
+ return;
+ }
+ var adjustedX = touch.pageX - (scrollX + rect.left);
+ var adjustedY = touch.pageY - (scrollY + rect.top);
+ adjustedX = adjustedX * (cw / rect.width);
+ adjustedY = adjustedY * (ch / rect.height);
+ var coords = {
+ x: adjustedX,
+ y: adjustedY
+ };
+ if (event.type === "touchstart") {
+ Browser.lastTouches[touch.identifier] = coords;
+ Browser.touches[touch.identifier] = coords;
+ } else if (event.type === "touchend" || event.type === "touchmove") {
+ var last = Browser.touches[touch.identifier];
+ if (!last) last = coords;
+ Browser.lastTouches[touch.identifier] = last;
+ Browser.touches[touch.identifier] = coords;
+ }
+ return;
+ }
+ var x = event.pageX - (scrollX + rect.left);
+ var y = event.pageY - (scrollY + rect.top);
+ x = x * (cw / rect.width);
+ y = y * (ch / rect.height);
+ Browser.mouseMovementX = x - Browser.mouseX;
+ Browser.mouseMovementY = y - Browser.mouseY;
+ Browser.mouseX = x;
+ Browser.mouseY = y;
+ }
+ },
+ resizeListeners: [],
+ updateResizeListeners: function() {
+ var canvas = Module["canvas"];
+ Browser.resizeListeners.forEach(function(listener) {
+ listener(canvas.width, canvas.height);
+ });
+ },
+ setCanvasSize: function(width, height, noUpdates) {
+ var canvas = Module["canvas"];
+ Browser.updateCanvasDimensions(canvas, width, height);
+ if (!noUpdates) Browser.updateResizeListeners();
+ },
+ windowedWidth: 0,
+ windowedHeight: 0,
+ setFullscreenCanvasSize: function() {
+ if (typeof SDL != "undefined") {
+ var flags = GROWABLE_HEAP_U32()[SDL.screen >> 2];
+ flags = flags | 8388608;
+ GROWABLE_HEAP_I32()[SDL.screen >> 2] = flags;
+ }
+ Browser.updateCanvasDimensions(Module["canvas"]);
+ Browser.updateResizeListeners();
+ },
+ setWindowedCanvasSize: function() {
+ if (typeof SDL != "undefined") {
+ var flags = GROWABLE_HEAP_U32()[SDL.screen >> 2];
+ flags = flags & ~8388608;
+ GROWABLE_HEAP_I32()[SDL.screen >> 2] = flags;
+ }
+ Browser.updateCanvasDimensions(Module["canvas"]);
+ Browser.updateResizeListeners();
+ },
+ updateCanvasDimensions: function(canvas, wNative, hNative) {
+ if (wNative && hNative) {
+ canvas.widthNative = wNative;
+ canvas.heightNative = hNative;
+ } else {
+ wNative = canvas.widthNative;
+ hNative = canvas.heightNative;
+ }
+ var w = wNative;
+ var h = hNative;
+ if (Module["forcedAspectRatio"] && Module["forcedAspectRatio"] > 0) {
+ if (w / h < Module["forcedAspectRatio"]) {
+ w = Math.round(h * Module["forcedAspectRatio"]);
+ } else {
+ h = Math.round(w / Module["forcedAspectRatio"]);
+ }
+ }
+ if ((document["fullscreenElement"] || document["mozFullScreenElement"] || document["msFullscreenElement"] || document["webkitFullscreenElement"] || document["webkitCurrentFullScreenElement"]) === canvas.parentNode && typeof screen != "undefined") {
+ var factor = Math.min(screen.width / w, screen.height / h);
+ w = Math.round(w * factor);
+ h = Math.round(h * factor);
+ }
+ if (Browser.resizeCanvas) {
+ if (canvas.width != w) canvas.width = w;
+ if (canvas.height != h) canvas.height = h;
+ if (typeof canvas.style != "undefined") {
+ canvas.style.removeProperty("width");
+ canvas.style.removeProperty("height");
+ }
+ } else {
+ if (canvas.width != wNative) canvas.width = wNative;
+ if (canvas.height != hNative) canvas.height = hNative;
+ if (typeof canvas.style != "undefined") {
+ if (w != wNative || h != hNative) {
+ canvas.style.setProperty("width", w + "px", "important");
+ canvas.style.setProperty("height", h + "px", "important");
+ } else {
+ canvas.style.removeProperty("width");
+ canvas.style.removeProperty("height");
+ }
+ }
+ }
+ }
+};
+
+function _emscripten_cancel_main_loop() {
+ Browser.mainLoop.pause();
+ Browser.mainLoop.func = null;
+}
+
+function _emscripten_check_blocking_allowed() {
+ if (ENVIRONMENT_IS_WORKER) return;
+ warnOnce("Blocking on the main thread is very dangerous, see https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread");
+}
+
+function _emscripten_console_error(str) {
+ assert(typeof str == "number");
+ console.error(UTF8ToString(str));
+}
+
+function _emscripten_force_exit(status) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(33, 1, status);
+ noExitRuntime = false;
+ runtimeKeepaliveCounter = 0;
+ exit(status);
+}
+
+function __webgl_enable_ANGLE_instanced_arrays(ctx) {
+ var ext = ctx.getExtension("ANGLE_instanced_arrays");
+ if (ext) {
+ ctx["vertexAttribDivisor"] = function(index, divisor) {
+ ext["vertexAttribDivisorANGLE"](index, divisor);
+ };
+ ctx["drawArraysInstanced"] = function(mode, first, count, primcount) {
+ ext["drawArraysInstancedANGLE"](mode, first, count, primcount);
+ };
+ ctx["drawElementsInstanced"] = function(mode, count, type, indices, primcount) {
+ ext["drawElementsInstancedANGLE"](mode, count, type, indices, primcount);
+ };
+ return 1;
+ }
+}
+
+function __webgl_enable_OES_vertex_array_object(ctx) {
+ var ext = ctx.getExtension("OES_vertex_array_object");
+ if (ext) {
+ ctx["createVertexArray"] = function() {
+ return ext["createVertexArrayOES"]();
+ };
+ ctx["deleteVertexArray"] = function(vao) {
+ ext["deleteVertexArrayOES"](vao);
+ };
+ ctx["bindVertexArray"] = function(vao) {
+ ext["bindVertexArrayOES"](vao);
+ };
+ ctx["isVertexArray"] = function(vao) {
+ return ext["isVertexArrayOES"](vao);
+ };
+ return 1;
+ }
+}
+
+function __webgl_enable_WEBGL_draw_buffers(ctx) {
+ var ext = ctx.getExtension("WEBGL_draw_buffers");
+ if (ext) {
+ ctx["drawBuffers"] = function(n, bufs) {
+ ext["drawBuffersWEBGL"](n, bufs);
+ };
+ return 1;
+ }
+}
+
+function __webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(ctx) {
+ return !!(ctx.dibvbi = ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));
+}
+
+function __webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(ctx) {
+ return !!(ctx.mdibvbi = ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));
+}
+
+function __webgl_enable_WEBGL_multi_draw(ctx) {
+ return !!(ctx.multiDrawWebgl = ctx.getExtension("WEBGL_multi_draw"));
+}
+
+var GL = {
+ counter: 1,
+ buffers: [],
+ programs: [],
+ framebuffers: [],
+ renderbuffers: [],
+ textures: [],
+ shaders: [],
+ vaos: [],
+ contexts: {},
+ offscreenCanvases: {},
+ queries: [],
+ samplers: [],
+ transformFeedbacks: [],
+ syncs: [],
+ stringCache: {},
+ stringiCache: {},
+ unpackAlignment: 4,
+ recordError: function recordError(errorCode) {
+ if (!GL.lastError) {
+ GL.lastError = errorCode;
+ }
+ },
+ getNewId: function(table) {
+ var ret = GL.counter++;
+ for (var i = table.length; i < ret; i++) {
+ table[i] = null;
+ }
+ return ret;
+ },
+ getSource: function(shader, count, string, length) {
+ var source = "";
+ for (var i = 0; i < count; ++i) {
+ var len = length ? GROWABLE_HEAP_I32()[length + i * 4 >> 2] : -1;
+ source += UTF8ToString(GROWABLE_HEAP_I32()[string + i * 4 >> 2], len < 0 ? undefined : len);
+ }
+ return source;
+ },
+ createContext: function(canvas, webGLContextAttributes) {
+ if (webGLContextAttributes.renderViaOffscreenBackBuffer) webGLContextAttributes["preserveDrawingBuffer"] = true;
+ var ctx = webGLContextAttributes.majorVersion > 1 ? canvas.getContext("webgl2", webGLContextAttributes) : canvas.getContext("webgl", webGLContextAttributes);
+ if (!ctx) return 0;
+ var handle = GL.registerContext(ctx, webGLContextAttributes);
+ return handle;
+ },
+ enableOffscreenFramebufferAttributes: function(webGLContextAttributes) {
+ webGLContextAttributes.renderViaOffscreenBackBuffer = true;
+ webGLContextAttributes.preserveDrawingBuffer = true;
+ },
+ createOffscreenFramebuffer: function(context) {
+ var gl = context.GLctx;
+ var fbo = gl.createFramebuffer();
+ gl.bindFramebuffer(36160, fbo);
+ context.defaultFbo = fbo;
+ context.defaultFboForbidBlitFramebuffer = false;
+ if (gl.getContextAttributes().antialias) {
+ context.defaultFboForbidBlitFramebuffer = true;
+ } else {
+ var firefoxMatch = navigator.userAgent.toLowerCase().match(/firefox\/(\d\d)/);
+ if (firefoxMatch != null) {
+ var firefoxVersion = firefoxMatch[1];
+ context.defaultFboForbidBlitFramebuffer = firefoxVersion < 67;
+ }
+ }
+ context.defaultColorTarget = gl.createTexture();
+ context.defaultDepthTarget = gl.createRenderbuffer();
+ GL.resizeOffscreenFramebuffer(context);
+ gl.bindTexture(3553, context.defaultColorTarget);
+ gl.texParameteri(3553, 10241, 9728);
+ gl.texParameteri(3553, 10240, 9728);
+ gl.texParameteri(3553, 10242, 33071);
+ gl.texParameteri(3553, 10243, 33071);
+ gl.texImage2D(3553, 0, 6408, gl.canvas.width, gl.canvas.height, 0, 6408, 5121, null);
+ gl.framebufferTexture2D(36160, 36064, 3553, context.defaultColorTarget, 0);
+ gl.bindTexture(3553, null);
+ var depthTarget = gl.createRenderbuffer();
+ gl.bindRenderbuffer(36161, context.defaultDepthTarget);
+ gl.renderbufferStorage(36161, 33189, gl.canvas.width, gl.canvas.height);
+ gl.framebufferRenderbuffer(36160, 36096, 36161, context.defaultDepthTarget);
+ gl.bindRenderbuffer(36161, null);
+ var vertices = [ -1, -1, -1, 1, 1, -1, 1, 1 ];
+ var vb = gl.createBuffer();
+ gl.bindBuffer(34962, vb);
+ gl.bufferData(34962, new Float32Array(vertices), 35044);
+ gl.bindBuffer(34962, null);
+ context.blitVB = vb;
+ var vsCode = "attribute vec2 pos;" + "varying lowp vec2 tex;" + "void main() { tex = pos * 0.5 + vec2(0.5,0.5); gl_Position = vec4(pos, 0.0, 1.0); }";
+ var vs = gl.createShader(35633);
+ gl.shaderSource(vs, vsCode);
+ gl.compileShader(vs);
+ var fsCode = "varying lowp vec2 tex;" + "uniform sampler2D sampler;" + "void main() { gl_FragColor = texture2D(sampler, tex); }";
+ var fs = gl.createShader(35632);
+ gl.shaderSource(fs, fsCode);
+ gl.compileShader(fs);
+ var blitProgram = gl.createProgram();
+ gl.attachShader(blitProgram, vs);
+ gl.attachShader(blitProgram, fs);
+ gl.linkProgram(blitProgram);
+ context.blitProgram = blitProgram;
+ context.blitPosLoc = gl.getAttribLocation(blitProgram, "pos");
+ gl.useProgram(blitProgram);
+ gl.uniform1i(gl.getUniformLocation(blitProgram, "sampler"), 0);
+ gl.useProgram(null);
+ context.defaultVao = undefined;
+ if (gl.createVertexArray) {
+ context.defaultVao = gl.createVertexArray();
+ gl.bindVertexArray(context.defaultVao);
+ gl.enableVertexAttribArray(context.blitPosLoc);
+ gl.bindVertexArray(null);
+ }
+ },
+ resizeOffscreenFramebuffer: function(context) {
+ var gl = context.GLctx;
+ if (context.defaultColorTarget) {
+ var prevTextureBinding = gl.getParameter(32873);
+ gl.bindTexture(3553, context.defaultColorTarget);
+ gl.texImage2D(3553, 0, 6408, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 6408, 5121, null);
+ gl.bindTexture(3553, prevTextureBinding);
+ }
+ if (context.defaultDepthTarget) {
+ var prevRenderBufferBinding = gl.getParameter(36007);
+ gl.bindRenderbuffer(36161, context.defaultDepthTarget);
+ gl.renderbufferStorage(36161, 33189, gl.drawingBufferWidth, gl.drawingBufferHeight);
+ gl.bindRenderbuffer(36161, prevRenderBufferBinding);
+ }
+ },
+ blitOffscreenFramebuffer: function(context) {
+ var gl = context.GLctx;
+ var prevScissorTest = gl.getParameter(3089);
+ if (prevScissorTest) gl.disable(3089);
+ var prevFbo = gl.getParameter(36006);
+ if (gl.blitFramebuffer && !context.defaultFboForbidBlitFramebuffer) {
+ gl.bindFramebuffer(36008, context.defaultFbo);
+ gl.bindFramebuffer(36009, null);
+ gl.blitFramebuffer(0, 0, gl.canvas.width, gl.canvas.height, 0, 0, gl.canvas.width, gl.canvas.height, 16384, 9728);
+ } else {
+ gl.bindFramebuffer(36160, null);
+ var prevProgram = gl.getParameter(35725);
+ gl.useProgram(context.blitProgram);
+ var prevVB = gl.getParameter(34964);
+ gl.bindBuffer(34962, context.blitVB);
+ var prevActiveTexture = gl.getParameter(34016);
+ gl.activeTexture(33984);
+ var prevTextureBinding = gl.getParameter(32873);
+ gl.bindTexture(3553, context.defaultColorTarget);
+ var prevBlend = gl.getParameter(3042);
+ if (prevBlend) gl.disable(3042);
+ var prevCullFace = gl.getParameter(2884);
+ if (prevCullFace) gl.disable(2884);
+ var prevDepthTest = gl.getParameter(2929);
+ if (prevDepthTest) gl.disable(2929);
+ var prevStencilTest = gl.getParameter(2960);
+ if (prevStencilTest) gl.disable(2960);
+ function draw() {
+ gl.vertexAttribPointer(context.blitPosLoc, 2, 5126, false, 0, 0);
+ gl.drawArrays(5, 0, 4);
+ }
+ if (context.defaultVao) {
+ var prevVAO = gl.getParameter(34229);
+ gl.bindVertexArray(context.defaultVao);
+ draw();
+ gl.bindVertexArray(prevVAO);
+ } else {
+ var prevVertexAttribPointer = {
+ buffer: gl.getVertexAttrib(context.blitPosLoc, 34975),
+ size: gl.getVertexAttrib(context.blitPosLoc, 34339),
+ stride: gl.getVertexAttrib(context.blitPosLoc, 34340),
+ type: gl.getVertexAttrib(context.blitPosLoc, 34341),
+ normalized: gl.getVertexAttrib(context.blitPosLoc, 34922),
+ pointer: gl.getVertexAttribOffset(context.blitPosLoc, 34373)
+ };
+ var maxVertexAttribs = gl.getParameter(34921);
+ var prevVertexAttribEnables = [];
+ for (var i = 0; i < maxVertexAttribs; ++i) {
+ var prevEnabled = gl.getVertexAttrib(i, 34338);
+ var wantEnabled = i == context.blitPosLoc;
+ if (prevEnabled && !wantEnabled) {
+ gl.disableVertexAttribArray(i);
+ }
+ if (!prevEnabled && wantEnabled) {
+ gl.enableVertexAttribArray(i);
+ }
+ prevVertexAttribEnables[i] = prevEnabled;
+ }
+ draw();
+ for (var i = 0; i < maxVertexAttribs; ++i) {
+ var prevEnabled = prevVertexAttribEnables[i];
+ var nowEnabled = i == context.blitPosLoc;
+ if (prevEnabled && !nowEnabled) {
+ gl.enableVertexAttribArray(i);
+ }
+ if (!prevEnabled && nowEnabled) {
+ gl.disableVertexAttribArray(i);
+ }
+ }
+ gl.bindBuffer(34962, prevVertexAttribPointer.buffer);
+ gl.vertexAttribPointer(context.blitPosLoc, prevVertexAttribPointer.size, prevVertexAttribPointer.type, prevVertexAttribPointer.normalized, prevVertexAttribPointer.stride, prevVertexAttribPointer.offset);
+ }
+ if (prevStencilTest) gl.enable(2960);
+ if (prevDepthTest) gl.enable(2929);
+ if (prevCullFace) gl.enable(2884);
+ if (prevBlend) gl.enable(3042);
+ gl.bindTexture(3553, prevTextureBinding);
+ gl.activeTexture(prevActiveTexture);
+ gl.bindBuffer(34962, prevVB);
+ gl.useProgram(prevProgram);
+ }
+ gl.bindFramebuffer(36160, prevFbo);
+ if (prevScissorTest) gl.enable(3089);
+ },
+ registerContext: function(ctx, webGLContextAttributes) {
+ var handle = _malloc(8);
+ GROWABLE_HEAP_I32()[handle + 4 >> 2] = _pthread_self();
+ var context = {
+ handle: handle,
+ attributes: webGLContextAttributes,
+ version: webGLContextAttributes.majorVersion,
+ GLctx: ctx
+ };
+ if (ctx.canvas) ctx.canvas.GLctxObject = context;
+ GL.contexts[handle] = context;
+ if (typeof webGLContextAttributes.enableExtensionsByDefault == "undefined" || webGLContextAttributes.enableExtensionsByDefault) {
+ GL.initExtensions(context);
+ }
+ if (webGLContextAttributes.renderViaOffscreenBackBuffer) GL.createOffscreenFramebuffer(context);
+ return handle;
+ },
+ makeContextCurrent: function(contextHandle) {
+ GL.currentContext = GL.contexts[contextHandle];
+ Module.ctx = GLctx = GL.currentContext && GL.currentContext.GLctx;
+ return !(contextHandle && !GLctx);
+ },
+ getContext: function(contextHandle) {
+ return GL.contexts[contextHandle];
+ },
+ deleteContext: function(contextHandle) {
+ if (GL.currentContext === GL.contexts[contextHandle]) GL.currentContext = null;
+ if (typeof JSEvents == "object") JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas);
+ if (GL.contexts[contextHandle] && GL.contexts[contextHandle].GLctx.canvas) GL.contexts[contextHandle].GLctx.canvas.GLctxObject = undefined;
+ _free(GL.contexts[contextHandle].handle);
+ GL.contexts[contextHandle] = null;
+ },
+ initExtensions: function(context) {
+ if (!context) context = GL.currentContext;
+ if (context.initExtensionsDone) return;
+ context.initExtensionsDone = true;
+ var GLctx = context.GLctx;
+ __webgl_enable_ANGLE_instanced_arrays(GLctx);
+ __webgl_enable_OES_vertex_array_object(GLctx);
+ __webgl_enable_WEBGL_draw_buffers(GLctx);
+ __webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);
+ __webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);
+ if (context.version >= 2) {
+ GLctx.disjointTimerQueryExt = GLctx.getExtension("EXT_disjoint_timer_query_webgl2");
+ }
+ if (context.version < 2 || !GLctx.disjointTimerQueryExt) {
+ GLctx.disjointTimerQueryExt = GLctx.getExtension("EXT_disjoint_timer_query");
+ }
+ __webgl_enable_WEBGL_multi_draw(GLctx);
+ var exts = GLctx.getSupportedExtensions() || [];
+ exts.forEach(function(ext) {
+ if (!ext.includes("lose_context") && !ext.includes("debug")) {
+ GLctx.getExtension(ext);
+ }
+ });
+ }
+};
+
+function _emscripten_glActiveTexture(x0) {
+ GLctx["activeTexture"](x0);
+}
+
+function _emscripten_glAttachShader(program, shader) {
+ GLctx.attachShader(GL.programs[program], GL.shaders[shader]);
+}
+
+function _emscripten_glBeginTransformFeedback(x0) {
+ GLctx["beginTransformFeedback"](x0);
+}
+
+function _emscripten_glBindBuffer(target, buffer) {
+ if (target == 35051) {
+ GLctx.currentPixelPackBufferBinding = buffer;
+ } else if (target == 35052) {
+ GLctx.currentPixelUnpackBufferBinding = buffer;
+ }
+ GLctx.bindBuffer(target, GL.buffers[buffer]);
+}
+
+function _emscripten_glBindBufferBase(target, index, buffer) {
+ GLctx["bindBufferBase"](target, index, GL.buffers[buffer]);
+}
+
+function _emscripten_glBindBufferRange(target, index, buffer, offset, ptrsize) {
+ GLctx["bindBufferRange"](target, index, GL.buffers[buffer], offset, ptrsize);
+}
+
+function _emscripten_glBindFramebuffer(target, framebuffer) {
+ GLctx.bindFramebuffer(target, framebuffer ? GL.framebuffers[framebuffer] : GL.currentContext.defaultFbo);
+}
+
+function _emscripten_glBindRenderbuffer(target, renderbuffer) {
+ GLctx.bindRenderbuffer(target, GL.renderbuffers[renderbuffer]);
+}
+
+function _emscripten_glBindTexture(target, texture) {
+ GLctx.bindTexture(target, GL.textures[texture]);
+}
+
+function _emscripten_glBindVertexArray(vao) {
+ GLctx["bindVertexArray"](GL.vaos[vao]);
+}
+
+function _emscripten_glBlendColor(x0, x1, x2, x3) {
+ GLctx["blendColor"](x0, x1, x2, x3);
+}
+
+function _emscripten_glBlendEquation(x0) {
+ GLctx["blendEquation"](x0);
+}
+
+function _emscripten_glBlendFunc(x0, x1) {
+ GLctx["blendFunc"](x0, x1);
+}
+
+function _emscripten_glBlendFuncSeparate(x0, x1, x2, x3) {
+ GLctx["blendFuncSeparate"](x0, x1, x2, x3);
+}
+
+function _emscripten_glBlitFramebuffer(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) {
+ GLctx["blitFramebuffer"](x0, x1, x2, x3, x4, x5, x6, x7, x8, x9);
+}
+
+function _emscripten_glBufferData(target, size, data, usage) {
+ if (GL.currentContext.version >= 2) {
+ if (data) {
+ GLctx.bufferData(target, GROWABLE_HEAP_U8(), usage, data, size);
+ } else {
+ GLctx.bufferData(target, size, usage);
+ }
+ } else {
+ GLctx.bufferData(target, data ? GROWABLE_HEAP_U8().subarray(data, data + size) : size, usage);
+ }
+}
+
+function _emscripten_glBufferSubData(target, offset, size, data) {
+ if (GL.currentContext.version >= 2) {
+ GLctx.bufferSubData(target, offset, GROWABLE_HEAP_U8(), data, size);
+ return;
+ }
+ GLctx.bufferSubData(target, offset, GROWABLE_HEAP_U8().subarray(data, data + size));
+}
+
+function _emscripten_glCheckFramebufferStatus(x0) {
+ return GLctx["checkFramebufferStatus"](x0);
+}
+
+function _emscripten_glClear(x0) {
+ GLctx["clear"](x0);
+}
+
+function _emscripten_glClearBufferfv(buffer, drawbuffer, value) {
+ GLctx["clearBufferfv"](buffer, drawbuffer, GROWABLE_HEAP_F32(), value >> 2);
+}
+
+function _emscripten_glClearColor(x0, x1, x2, x3) {
+ GLctx["clearColor"](x0, x1, x2, x3);
+}
+
+function _emscripten_glClearDepthf(x0) {
+ GLctx["clearDepth"](x0);
+}
+
+function _emscripten_glColorMask(red, green, blue, alpha) {
+ GLctx.colorMask(!!red, !!green, !!blue, !!alpha);
+}
+
+function _emscripten_glCompileShader(shader) {
+ GLctx.compileShader(GL.shaders[shader]);
+}
+
+function _emscripten_glCompressedTexImage2D(target, level, internalFormat, width, height, border, imageSize, data) {
+ if (GL.currentContext.version >= 2) {
+ if (GLctx.currentPixelUnpackBufferBinding) {
+ GLctx["compressedTexImage2D"](target, level, internalFormat, width, height, border, imageSize, data);
+ } else {
+ GLctx["compressedTexImage2D"](target, level, internalFormat, width, height, border, GROWABLE_HEAP_U8(), data, imageSize);
+ }
+ return;
+ }
+ GLctx["compressedTexImage2D"](target, level, internalFormat, width, height, border, data ? GROWABLE_HEAP_U8().subarray(data, data + imageSize) : null);
+}
+
+function _emscripten_glCopyBufferSubData(x0, x1, x2, x3, x4) {
+ GLctx["copyBufferSubData"](x0, x1, x2, x3, x4);
+}
+
+function _emscripten_glCreateProgram() {
+ var id = GL.getNewId(GL.programs);
+ var program = GLctx.createProgram();
+ program.name = id;
+ program.maxUniformLength = program.maxAttributeLength = program.maxUniformBlockNameLength = 0;
+ program.uniformIdCounter = 1;
+ GL.programs[id] = program;
+ return id;
+}
+
+function _emscripten_glCreateShader(shaderType) {
+ var id = GL.getNewId(GL.shaders);
+ GL.shaders[id] = GLctx.createShader(shaderType);
+ return id;
+}
+
+function _emscripten_glCullFace(x0) {
+ GLctx["cullFace"](x0);
+}
+
+function _emscripten_glDeleteBuffers(n, buffers) {
+ for (var i = 0; i < n; i++) {
+ var id = GROWABLE_HEAP_I32()[buffers + i * 4 >> 2];
+ var buffer = GL.buffers[id];
+ if (!buffer) continue;
+ GLctx.deleteBuffer(buffer);
+ buffer.name = 0;
+ GL.buffers[id] = null;
+ if (id == GLctx.currentPixelPackBufferBinding) GLctx.currentPixelPackBufferBinding = 0;
+ if (id == GLctx.currentPixelUnpackBufferBinding) GLctx.currentPixelUnpackBufferBinding = 0;
+ }
+}
+
+function _emscripten_glDeleteFramebuffers(n, framebuffers) {
+ for (var i = 0; i < n; ++i) {
+ var id = GROWABLE_HEAP_I32()[framebuffers + i * 4 >> 2];
+ var framebuffer = GL.framebuffers[id];
+ if (!framebuffer) continue;
+ GLctx.deleteFramebuffer(framebuffer);
+ framebuffer.name = 0;
+ GL.framebuffers[id] = null;
+ }
+}
+
+function _emscripten_glDeleteProgram(id) {
+ if (!id) return;
+ var program = GL.programs[id];
+ if (!program) {
+ GL.recordError(1281);
+ return;
+ }
+ GLctx.deleteProgram(program);
+ program.name = 0;
+ GL.programs[id] = null;
+}
+
+function _emscripten_glDeleteQueries(n, ids) {
+ for (var i = 0; i < n; i++) {
+ var id = GROWABLE_HEAP_I32()[ids + i * 4 >> 2];
+ var query = GL.queries[id];
+ if (!query) continue;
+ GLctx["deleteQuery"](query);
+ GL.queries[id] = null;
+ }
+}
+
+function _emscripten_glDeleteRenderbuffers(n, renderbuffers) {
+ for (var i = 0; i < n; i++) {
+ var id = GROWABLE_HEAP_I32()[renderbuffers + i * 4 >> 2];
+ var renderbuffer = GL.renderbuffers[id];
+ if (!renderbuffer) continue;
+ GLctx.deleteRenderbuffer(renderbuffer);
+ renderbuffer.name = 0;
+ GL.renderbuffers[id] = null;
+ }
+}
+
+function _emscripten_glDeleteShader(id) {
+ if (!id) return;
+ var shader = GL.shaders[id];
+ if (!shader) {
+ GL.recordError(1281);
+ return;
+ }
+ GLctx.deleteShader(shader);
+ GL.shaders[id] = null;
+}
+
+function _emscripten_glDeleteSync(id) {
+ if (!id) return;
+ var sync = GL.syncs[id];
+ if (!sync) {
+ GL.recordError(1281);
+ return;
+ }
+ GLctx.deleteSync(sync);
+ sync.name = 0;
+ GL.syncs[id] = null;
+}
+
+function _emscripten_glDeleteTextures(n, textures) {
+ for (var i = 0; i < n; i++) {
+ var id = GROWABLE_HEAP_I32()[textures + i * 4 >> 2];
+ var texture = GL.textures[id];
+ if (!texture) continue;
+ GLctx.deleteTexture(texture);
+ texture.name = 0;
+ GL.textures[id] = null;
+ }
+}
+
+function _emscripten_glDeleteVertexArrays(n, vaos) {
+ for (var i = 0; i < n; i++) {
+ var id = GROWABLE_HEAP_I32()[vaos + i * 4 >> 2];
+ GLctx["deleteVertexArray"](GL.vaos[id]);
+ GL.vaos[id] = null;
+ }
+}
+
+function _emscripten_glDepthFunc(x0) {
+ GLctx["depthFunc"](x0);
+}
+
+function _emscripten_glDepthMask(flag) {
+ GLctx.depthMask(!!flag);
+}
+
+function _emscripten_glDisable(x0) {
+ GLctx["disable"](x0);
+}
+
+function _emscripten_glDisableVertexAttribArray(index) {
+ GLctx.disableVertexAttribArray(index);
+}
+
+function _emscripten_glDrawArrays(mode, first, count) {
+ GLctx.drawArrays(mode, first, count);
+}
+
+function _emscripten_glDrawArraysInstanced(mode, first, count, primcount) {
+ GLctx["drawArraysInstanced"](mode, first, count, primcount);
+}
+
+function _emscripten_glDrawElements(mode, count, type, indices) {
+ GLctx.drawElements(mode, count, type, indices);
+}
+
+function _emscripten_glDrawElementsInstanced(mode, count, type, indices, primcount) {
+ GLctx["drawElementsInstanced"](mode, count, type, indices, primcount);
+}
+
+function _emscripten_glEnable(x0) {
+ GLctx["enable"](x0);
+}
+
+function _emscripten_glEnableVertexAttribArray(index) {
+ GLctx.enableVertexAttribArray(index);
+}
+
+function _emscripten_glEndTransformFeedback() {
+ GLctx["endTransformFeedback"]();
+}
+
+function _emscripten_glFenceSync(condition, flags) {
+ var sync = GLctx.fenceSync(condition, flags);
+ if (sync) {
+ var id = GL.getNewId(GL.syncs);
+ sync.name = id;
+ GL.syncs[id] = sync;
+ return id;
+ } else {
+ return 0;
+ }
+}
+
+function _emscripten_glFinish() {
+ GLctx["finish"]();
+}
+
+function _emscripten_glFramebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer) {
+ GLctx.framebufferRenderbuffer(target, attachment, renderbuffertarget, GL.renderbuffers[renderbuffer]);
+}
+
+function _emscripten_glFramebufferTexture2D(target, attachment, textarget, texture, level) {
+ GLctx.framebufferTexture2D(target, attachment, textarget, GL.textures[texture], level);
+}
+
+function _emscripten_glFramebufferTextureLayer(target, attachment, texture, level, layer) {
+ GLctx.framebufferTextureLayer(target, attachment, GL.textures[texture], level, layer);
+}
+
+function _emscripten_glFrontFace(x0) {
+ GLctx["frontFace"](x0);
+}
+
+function __glGenObject(n, buffers, createFunction, objectTable) {
+ for (var i = 0; i < n; i++) {
+ var buffer = GLctx[createFunction]();
+ var id = buffer && GL.getNewId(objectTable);
+ if (buffer) {
+ buffer.name = id;
+ objectTable[id] = buffer;
+ } else {
+ GL.recordError(1282);
+ }
+ GROWABLE_HEAP_I32()[buffers + i * 4 >> 2] = id;
+ }
+}
+
+function _emscripten_glGenBuffers(n, buffers) {
+ __glGenObject(n, buffers, "createBuffer", GL.buffers);
+}
+
+function _emscripten_glGenFramebuffers(n, ids) {
+ __glGenObject(n, ids, "createFramebuffer", GL.framebuffers);
+}
+
+function _emscripten_glGenQueries(n, ids) {
+ __glGenObject(n, ids, "createQuery", GL.queries);
+}
+
+function _emscripten_glGenRenderbuffers(n, renderbuffers) {
+ __glGenObject(n, renderbuffers, "createRenderbuffer", GL.renderbuffers);
+}
+
+function _emscripten_glGenTextures(n, textures) {
+ __glGenObject(n, textures, "createTexture", GL.textures);
+}
+
+function _emscripten_glGenVertexArrays(n, arrays) {
+ __glGenObject(n, arrays, "createVertexArray", GL.vaos);
+}
+
+function _emscripten_glGenerateMipmap(x0) {
+ GLctx["generateMipmap"](x0);
+}
+
+function readI53FromI64(ptr) {
+ return GROWABLE_HEAP_U32()[ptr >> 2] + GROWABLE_HEAP_I32()[ptr + 4 >> 2] * 4294967296;
+}
+
+function readI53FromU64(ptr) {
+ return GROWABLE_HEAP_U32()[ptr >> 2] + GROWABLE_HEAP_U32()[ptr + 4 >> 2] * 4294967296;
+}
+
+function writeI53ToI64(ptr, num) {
+ GROWABLE_HEAP_U32()[ptr >> 2] = num;
+ GROWABLE_HEAP_U32()[ptr + 4 >> 2] = (num - GROWABLE_HEAP_U32()[ptr >> 2]) / 4294967296;
+ var deserialized = num >= 0 ? readI53FromU64(ptr) : readI53FromI64(ptr);
+ if (deserialized != num) warnOnce("writeI53ToI64() out of range: serialized JS Number " + num + " to Wasm heap as bytes lo=0x" + GROWABLE_HEAP_U32()[ptr >> 2].toString(16) + ", hi=0x" + GROWABLE_HEAP_U32()[ptr + 4 >> 2].toString(16) + ", which deserializes back to " + deserialized + " instead!");
+}
+
+function emscriptenWebGLGet(name_, p, type) {
+ if (!p) {
+ GL.recordError(1281);
+ return;
+ }
+ var ret = undefined;
+ switch (name_) {
+ case 36346:
+ ret = 1;
+ break;
+
+ case 36344:
+ if (type != 0 && type != 1) {
+ GL.recordError(1280);
+ }
+ return;
+
+ case 34814:
+ case 36345:
+ ret = 0;
+ break;
+
+ case 34466:
+ var formats = GLctx.getParameter(34467);
+ ret = formats ? formats.length : 0;
+ break;
+
+ case 33309:
+ if (GL.currentContext.version < 2) {
+ GL.recordError(1282);
+ return;
+ }
+ var exts = GLctx.getSupportedExtensions() || [];
+ ret = 2 * exts.length;
+ break;
+
+ case 33307:
+ case 33308:
+ if (GL.currentContext.version < 2) {
+ GL.recordError(1280);
+ return;
+ }
+ ret = name_ == 33307 ? 3 : 0;
+ break;
+ }
+ if (ret === undefined) {
+ var result = GLctx.getParameter(name_);
+ switch (typeof result) {
+ case "number":
+ ret = result;
+ break;
+
+ case "boolean":
+ ret = result ? 1 : 0;
+ break;
+
+ case "string":
+ GL.recordError(1280);
+ return;
+
+ case "object":
+ if (result === null) {
+ switch (name_) {
+ case 34964:
+ case 35725:
+ case 34965:
+ case 36006:
+ case 36007:
+ case 32873:
+ case 34229:
+ case 36662:
+ case 36663:
+ case 35053:
+ case 35055:
+ case 36010:
+ case 35097:
+ case 35869:
+ case 32874:
+ case 36389:
+ case 35983:
+ case 35368:
+ case 34068:
+ {
+ ret = 0;
+ break;
+ }
+
+ default:
+ {
+ GL.recordError(1280);
+ return;
+ }
+ }
+ } else if (result instanceof Float32Array || result instanceof Uint32Array || result instanceof Int32Array || result instanceof Array) {
+ for (var i = 0; i < result.length; ++i) {
+ switch (type) {
+ case 0:
+ GROWABLE_HEAP_I32()[p + i * 4 >> 2] = result[i];
+ break;
+
+ case 2:
+ GROWABLE_HEAP_F32()[p + i * 4 >> 2] = result[i];
+ break;
+
+ case 4:
+ GROWABLE_HEAP_I8()[p + i >> 0] = result[i] ? 1 : 0;
+ break;
+ }
+ }
+ return;
+ } else {
+ try {
+ ret = result.name | 0;
+ } catch (e) {
+ GL.recordError(1280);
+ err("GL_INVALID_ENUM in glGet" + type + "v: Unknown object returned from WebGL getParameter(" + name_ + ")! (error: " + e + ")");
+ return;
+ }
+ }
+ break;
+
+ default:
+ GL.recordError(1280);
+ err("GL_INVALID_ENUM in glGet" + type + "v: Native code calling glGet" + type + "v(" + name_ + ") and it returns " + result + " of type " + typeof result + "!");
+ return;
+ }
+ }
+ switch (type) {
+ case 1:
+ writeI53ToI64(p, ret);
+ break;
+
+ case 0:
+ GROWABLE_HEAP_I32()[p >> 2] = ret;
+ break;
+
+ case 2:
+ GROWABLE_HEAP_F32()[p >> 2] = ret;
+ break;
+
+ case 4:
+ GROWABLE_HEAP_I8()[p >> 0] = ret ? 1 : 0;
+ break;
+ }
+}
+
+function _emscripten_glGetFloatv(name_, p) {
+ emscriptenWebGLGet(name_, p, 2);
+}
+
+function _emscripten_glGetInteger64v(name_, p) {
+ emscriptenWebGLGet(name_, p, 1);
+}
+
+function _emscripten_glGetProgramInfoLog(program, maxLength, length, infoLog) {
+ var log = GLctx.getProgramInfoLog(GL.programs[program]);
+ if (log === null) log = "(unknown error)";
+ var numBytesWrittenExclNull = maxLength > 0 && infoLog ? stringToUTF8(log, infoLog, maxLength) : 0;
+ if (length) GROWABLE_HEAP_I32()[length >> 2] = numBytesWrittenExclNull;
+}
+
+function _emscripten_glGetProgramiv(program, pname, p) {
+ if (!p) {
+ GL.recordError(1281);
+ return;
+ }
+ if (program >= GL.counter) {
+ GL.recordError(1281);
+ return;
+ }
+ program = GL.programs[program];
+ if (pname == 35716) {
+ var log = GLctx.getProgramInfoLog(program);
+ if (log === null) log = "(unknown error)";
+ GROWABLE_HEAP_I32()[p >> 2] = log.length + 1;
+ } else if (pname == 35719) {
+ if (!program.maxUniformLength) {
+ for (var i = 0; i < GLctx.getProgramParameter(program, 35718); ++i) {
+ program.maxUniformLength = Math.max(program.maxUniformLength, GLctx.getActiveUniform(program, i).name.length + 1);
+ }
+ }
+ GROWABLE_HEAP_I32()[p >> 2] = program.maxUniformLength;
+ } else if (pname == 35722) {
+ if (!program.maxAttributeLength) {
+ for (var i = 0; i < GLctx.getProgramParameter(program, 35721); ++i) {
+ program.maxAttributeLength = Math.max(program.maxAttributeLength, GLctx.getActiveAttrib(program, i).name.length + 1);
+ }
+ }
+ GROWABLE_HEAP_I32()[p >> 2] = program.maxAttributeLength;
+ } else if (pname == 35381) {
+ if (!program.maxUniformBlockNameLength) {
+ for (var i = 0; i < GLctx.getProgramParameter(program, 35382); ++i) {
+ program.maxUniformBlockNameLength = Math.max(program.maxUniformBlockNameLength, GLctx.getActiveUniformBlockName(program, i).length + 1);
+ }
+ }
+ GROWABLE_HEAP_I32()[p >> 2] = program.maxUniformBlockNameLength;
+ } else {
+ GROWABLE_HEAP_I32()[p >> 2] = GLctx.getProgramParameter(program, pname);
+ }
+}
+
+function _emscripten_glGetShaderInfoLog(shader, maxLength, length, infoLog) {
+ var log = GLctx.getShaderInfoLog(GL.shaders[shader]);
+ if (log === null) log = "(unknown error)";
+ var numBytesWrittenExclNull = maxLength > 0 && infoLog ? stringToUTF8(log, infoLog, maxLength) : 0;
+ if (length) GROWABLE_HEAP_I32()[length >> 2] = numBytesWrittenExclNull;
+}
+
+function _emscripten_glGetShaderiv(shader, pname, p) {
+ if (!p) {
+ GL.recordError(1281);
+ return;
+ }
+ if (pname == 35716) {
+ var log = GLctx.getShaderInfoLog(GL.shaders[shader]);
+ if (log === null) log = "(unknown error)";
+ var logLength = log ? log.length + 1 : 0;
+ GROWABLE_HEAP_I32()[p >> 2] = logLength;
+ } else if (pname == 35720) {
+ var source = GLctx.getShaderSource(GL.shaders[shader]);
+ var sourceLength = source ? source.length + 1 : 0;
+ GROWABLE_HEAP_I32()[p >> 2] = sourceLength;
+ } else {
+ GROWABLE_HEAP_I32()[p >> 2] = GLctx.getShaderParameter(GL.shaders[shader], pname);
+ }
+}
+
+function stringToNewUTF8(jsString) {
+ var length = lengthBytesUTF8(jsString) + 1;
+ var cString = _malloc(length);
+ stringToUTF8(jsString, cString, length);
+ return cString;
+}
+
+function _emscripten_glGetString(name_) {
+ var ret = GL.stringCache[name_];
+ if (!ret) {
+ switch (name_) {
+ case 7939:
+ var exts = GLctx.getSupportedExtensions() || [];
+ exts = exts.concat(exts.map(function(e) {
+ return "GL_" + e;
+ }));
+ ret = stringToNewUTF8(exts.join(" "));
+ break;
+
+ case 7936:
+ case 7937:
+ case 37445:
+ case 37446:
+ var s = GLctx.getParameter(name_);
+ if (!s) {
+ GL.recordError(1280);
+ }
+ ret = s && stringToNewUTF8(s);
+ break;
+
+ case 7938:
+ var glVersion = GLctx.getParameter(7938);
+ if (GL.currentContext.version >= 2) glVersion = "OpenGL ES 3.0 (" + glVersion + ")"; else {
+ glVersion = "OpenGL ES 2.0 (" + glVersion + ")";
+ }
+ ret = stringToNewUTF8(glVersion);
+ break;
+
+ case 35724:
+ var glslVersion = GLctx.getParameter(35724);
+ var ver_re = /^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;
+ var ver_num = glslVersion.match(ver_re);
+ if (ver_num !== null) {
+ if (ver_num[1].length == 3) ver_num[1] = ver_num[1] + "0";
+ glslVersion = "OpenGL ES GLSL ES " + ver_num[1] + " (" + glslVersion + ")";
+ }
+ ret = stringToNewUTF8(glslVersion);
+ break;
+
+ default:
+ GL.recordError(1280);
+ }
+ GL.stringCache[name_] = ret;
+ }
+ return ret;
+}
+
+function _emscripten_glGetStringi(name, index) {
+ if (GL.currentContext.version < 2) {
+ GL.recordError(1282);
+ return 0;
+ }
+ var stringiCache = GL.stringiCache[name];
+ if (stringiCache) {
+ if (index < 0 || index >= stringiCache.length) {
+ GL.recordError(1281);
+ return 0;
+ }
+ return stringiCache[index];
+ }
+ switch (name) {
+ case 7939:
+ var exts = GLctx.getSupportedExtensions() || [];
+ exts = exts.concat(exts.map(function(e) {
+ return "GL_" + e;
+ }));
+ exts = exts.map(function(e) {
+ return stringToNewUTF8(e);
+ });
+ stringiCache = GL.stringiCache[name] = exts;
+ if (index < 0 || index >= stringiCache.length) {
+ GL.recordError(1281);
+ return 0;
+ }
+ return stringiCache[index];
+
+ default:
+ GL.recordError(1280);
+ return 0;
+ }
+}
+
+function _emscripten_glGetSynciv(sync, pname, bufSize, length, values) {
+ if (bufSize < 0) {
+ GL.recordError(1281);
+ return;
+ }
+ if (!values) {
+ GL.recordError(1281);
+ return;
+ }
+ var ret = GLctx.getSyncParameter(GL.syncs[sync], pname);
+ if (ret !== null) {
+ GROWABLE_HEAP_I32()[values >> 2] = ret;
+ if (length) GROWABLE_HEAP_I32()[length >> 2] = 1;
+ }
+}
+
+function _emscripten_glGetUniformBlockIndex(program, uniformBlockName) {
+ return GLctx["getUniformBlockIndex"](GL.programs[program], UTF8ToString(uniformBlockName));
+}
+
+function webglGetLeftBracePos(name) {
+ return name.slice(-1) == "]" && name.lastIndexOf("[");
+}
+
+function webglPrepareUniformLocationsBeforeFirstUse(program) {
+ var uniformLocsById = program.uniformLocsById, uniformSizeAndIdsByName = program.uniformSizeAndIdsByName, i, j;
+ if (!uniformLocsById) {
+ program.uniformLocsById = uniformLocsById = {};
+ program.uniformArrayNamesById = {};
+ for (i = 0; i < GLctx.getProgramParameter(program, 35718); ++i) {
+ var u = GLctx.getActiveUniform(program, i);
+ var nm = u.name;
+ var sz = u.size;
+ var lb = webglGetLeftBracePos(nm);
+ var arrayName = lb > 0 ? nm.slice(0, lb) : nm;
+ var id = program.uniformIdCounter;
+ program.uniformIdCounter += sz;
+ uniformSizeAndIdsByName[arrayName] = [ sz, id ];
+ for (j = 0; j < sz; ++j) {
+ uniformLocsById[id] = j;
+ program.uniformArrayNamesById[id++] = arrayName;
+ }
+ }
+ }
+}
+
+function _emscripten_glGetUniformLocation(program, name) {
+ name = UTF8ToString(name);
+ if (program = GL.programs[program]) {
+ webglPrepareUniformLocationsBeforeFirstUse(program);
+ var uniformLocsById = program.uniformLocsById;
+ var arrayIndex = 0;
+ var uniformBaseName = name;
+ var leftBrace = webglGetLeftBracePos(name);
+ if (leftBrace > 0) {
+ arrayIndex = jstoi_q(name.slice(leftBrace + 1)) >>> 0;
+ uniformBaseName = name.slice(0, leftBrace);
+ }
+ var sizeAndId = program.uniformSizeAndIdsByName[uniformBaseName];
+ if (sizeAndId && arrayIndex < sizeAndId[0]) {
+ arrayIndex += sizeAndId[1];
+ if (uniformLocsById[arrayIndex] = uniformLocsById[arrayIndex] || GLctx.getUniformLocation(program, name)) {
+ return arrayIndex;
+ }
+ }
+ } else {
+ GL.recordError(1281);
+ }
+ return -1;
+}
+
+function _emscripten_glLinkProgram(program) {
+ program = GL.programs[program];
+ GLctx.linkProgram(program);
+ program.uniformLocsById = 0;
+ program.uniformSizeAndIdsByName = {};
+}
+
+function _emscripten_glPixelStorei(pname, param) {
+ if (pname == 3317) {
+ GL.unpackAlignment = param;
+ }
+ GLctx.pixelStorei(pname, param);
+}
+
+function _emscripten_glReadBuffer(x0) {
+ GLctx["readBuffer"](x0);
+}
+
+function computeUnpackAlignedImageSize(width, height, sizePerPixel, alignment) {
+ function roundedToNextMultipleOf(x, y) {
+ return x + y - 1 & -y;
+ }
+ var plainRowSize = width * sizePerPixel;
+ var alignedRowSize = roundedToNextMultipleOf(plainRowSize, alignment);
+ return height * alignedRowSize;
+}
+
+function __colorChannelsInGlTextureFormat(format) {
+ var colorChannels = {
+ 5: 3,
+ 6: 4,
+ 8: 2,
+ 29502: 3,
+ 29504: 4,
+ 26917: 2,
+ 26918: 2,
+ 29846: 3,
+ 29847: 4
+ };
+ return colorChannels[format - 6402] || 1;
+}
+
+function heapObjectForWebGLType(type) {
+ type -= 5120;
+ if (type == 0) return GROWABLE_HEAP_I8();
+ if (type == 1) return GROWABLE_HEAP_U8();
+ if (type == 2) return GROWABLE_HEAP_I16();
+ if (type == 4) return GROWABLE_HEAP_I32();
+ if (type == 6) return GROWABLE_HEAP_F32();
+ if (type == 5 || type == 28922 || type == 28520 || type == 30779 || type == 30782) return GROWABLE_HEAP_U32();
+ return GROWABLE_HEAP_U16();
+}
+
+function heapAccessShiftForWebGLHeap(heap) {
+ return 31 - Math.clz32(heap.BYTES_PER_ELEMENT);
+}
+
+function emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, internalFormat) {
+ var heap = heapObjectForWebGLType(type);
+ var shift = heapAccessShiftForWebGLHeap(heap);
+ var byteSize = 1 << shift;
+ var sizePerPixel = __colorChannelsInGlTextureFormat(format) * byteSize;
+ var bytes = computeUnpackAlignedImageSize(width, height, sizePerPixel, GL.unpackAlignment);
+ return heap.subarray(pixels >> shift, pixels + bytes >> shift);
+}
+
+function _emscripten_glReadPixels(x, y, width, height, format, type, pixels) {
+ if (GL.currentContext.version >= 2) {
+ if (GLctx.currentPixelPackBufferBinding) {
+ GLctx.readPixels(x, y, width, height, format, type, pixels);
+ } else {
+ var heap = heapObjectForWebGLType(type);
+ GLctx.readPixels(x, y, width, height, format, type, heap, pixels >> heapAccessShiftForWebGLHeap(heap));
+ }
+ return;
+ }
+ var pixelData = emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, format);
+ if (!pixelData) {
+ GL.recordError(1280);
+ return;
+ }
+ GLctx.readPixels(x, y, width, height, format, type, pixelData);
+}
+
+function _emscripten_glRenderbufferStorage(x0, x1, x2, x3) {
+ GLctx["renderbufferStorage"](x0, x1, x2, x3);
+}
+
+function _emscripten_glScissor(x0, x1, x2, x3) {
+ GLctx["scissor"](x0, x1, x2, x3);
+}
+
+function _emscripten_glShaderSource(shader, count, string, length) {
+ var source = GL.getSource(shader, count, string, length);
+ GLctx.shaderSource(GL.shaders[shader], source);
+}
+
+function _emscripten_glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels) {
+ if (GL.currentContext.version >= 2) {
+ if (GLctx.currentPixelUnpackBufferBinding) {
+ GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixels);
+ } else if (pixels) {
+ var heap = heapObjectForWebGLType(type);
+ GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, heap, pixels >> heapAccessShiftForWebGLHeap(heap));
+ } else {
+ GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, null);
+ }
+ return;
+ }
+ GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, internalFormat) : null);
+}
+
+function _emscripten_glTexImage3D(target, level, internalFormat, width, height, depth, border, format, type, pixels) {
+ if (GLctx.currentPixelUnpackBufferBinding) {
+ GLctx["texImage3D"](target, level, internalFormat, width, height, depth, border, format, type, pixels);
+ } else if (pixels) {
+ var heap = heapObjectForWebGLType(type);
+ GLctx["texImage3D"](target, level, internalFormat, width, height, depth, border, format, type, heap, pixels >> heapAccessShiftForWebGLHeap(heap));
+ } else {
+ GLctx["texImage3D"](target, level, internalFormat, width, height, depth, border, format, type, null);
+ }
+}
+
+function _emscripten_glTexParameterf(x0, x1, x2) {
+ GLctx["texParameterf"](x0, x1, x2);
+}
+
+function _emscripten_glTexParameteri(x0, x1, x2) {
+ GLctx["texParameteri"](x0, x1, x2);
+}
+
+function _emscripten_glTexStorage2D(x0, x1, x2, x3, x4) {
+ GLctx["texStorage2D"](x0, x1, x2, x3, x4);
+}
+
+function _emscripten_glTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels) {
+ if (GLctx.currentPixelUnpackBufferBinding) {
+ GLctx["texSubImage3D"](target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels);
+ } else if (pixels) {
+ var heap = heapObjectForWebGLType(type);
+ GLctx["texSubImage3D"](target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, heap, pixels >> heapAccessShiftForWebGLHeap(heap));
+ } else {
+ GLctx["texSubImage3D"](target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, null);
+ }
+}
+
+function _emscripten_glTransformFeedbackVaryings(program, count, varyings, bufferMode) {
+ program = GL.programs[program];
+ var vars = [];
+ for (var i = 0; i < count; i++) vars.push(UTF8ToString(GROWABLE_HEAP_I32()[varyings + i * 4 >> 2]));
+ GLctx["transformFeedbackVaryings"](program, vars, bufferMode);
+}
+
+function webglGetUniformLocation(location) {
+ var p = GLctx.currentProgram;
+ if (p) {
+ var webglLoc = p.uniformLocsById[location];
+ if (typeof webglLoc == "number") {
+ p.uniformLocsById[location] = webglLoc = GLctx.getUniformLocation(p, p.uniformArrayNamesById[location] + (webglLoc > 0 ? "[" + webglLoc + "]" : ""));
+ }
+ return webglLoc;
+ } else {
+ GL.recordError(1282);
+ }
+}
+
+function _emscripten_glUniform1f(location, v0) {
+ GLctx.uniform1f(webglGetUniformLocation(location), v0);
+}
+
+function _emscripten_glUniform1i(location, v0) {
+ GLctx.uniform1i(webglGetUniformLocation(location), v0);
+}
+
+var __miniTempWebGLIntBuffers = [];
+
+function _emscripten_glUniform1iv(location, count, value) {
+ if (GL.currentContext.version >= 2) {
+ GLctx.uniform1iv(webglGetUniformLocation(location), GROWABLE_HEAP_I32(), value >> 2, count);
+ return;
+ }
+ if (count <= 288) {
+ var view = __miniTempWebGLIntBuffers[count - 1];
+ for (var i = 0; i < count; ++i) {
+ view[i] = GROWABLE_HEAP_I32()[value + 4 * i >> 2];
+ }
+ } else {
+ var view = GROWABLE_HEAP_I32().subarray(value >> 2, value + count * 4 >> 2);
+ }
+ GLctx.uniform1iv(webglGetUniformLocation(location), view);
+}
+
+function _emscripten_glUniform1ui(location, v0) {
+ GLctx.uniform1ui(webglGetUniformLocation(location), v0);
+}
+
+function _emscripten_glUniform1uiv(location, count, value) {
+ GLctx.uniform1uiv(webglGetUniformLocation(location), GROWABLE_HEAP_U32(), value >> 2, count);
+}
+
+function _emscripten_glUniform2f(location, v0, v1) {
+ GLctx.uniform2f(webglGetUniformLocation(location), v0, v1);
+}
+
+var miniTempWebGLFloatBuffers = [];
+
+function _emscripten_glUniform2fv(location, count, value) {
+ if (GL.currentContext.version >= 2) {
+ GLctx.uniform2fv(webglGetUniformLocation(location), GROWABLE_HEAP_F32(), value >> 2, count * 2);
+ return;
+ }
+ if (count <= 144) {
+ var view = miniTempWebGLFloatBuffers[2 * count - 1];
+ for (var i = 0; i < 2 * count; i += 2) {
+ view[i] = GROWABLE_HEAP_F32()[value + 4 * i >> 2];
+ view[i + 1] = GROWABLE_HEAP_F32()[value + (4 * i + 4) >> 2];
+ }
+ } else {
+ var view = GROWABLE_HEAP_F32().subarray(value >> 2, value + count * 8 >> 2);
+ }
+ GLctx.uniform2fv(webglGetUniformLocation(location), view);
+}
+
+function _emscripten_glUniform2iv(location, count, value) {
+ if (GL.currentContext.version >= 2) {
+ GLctx.uniform2iv(webglGetUniformLocation(location), GROWABLE_HEAP_I32(), value >> 2, count * 2);
+ return;
+ }
+ if (count <= 144) {
+ var view = __miniTempWebGLIntBuffers[2 * count - 1];
+ for (var i = 0; i < 2 * count; i += 2) {
+ view[i] = GROWABLE_HEAP_I32()[value + 4 * i >> 2];
+ view[i + 1] = GROWABLE_HEAP_I32()[value + (4 * i + 4) >> 2];
+ }
+ } else {
+ var view = GROWABLE_HEAP_I32().subarray(value >> 2, value + count * 8 >> 2);
+ }
+ GLctx.uniform2iv(webglGetUniformLocation(location), view);
+}
+
+function _emscripten_glUniform3fv(location, count, value) {
+ if (GL.currentContext.version >= 2) {
+ GLctx.uniform3fv(webglGetUniformLocation(location), GROWABLE_HEAP_F32(), value >> 2, count * 3);
+ return;
+ }
+ if (count <= 96) {
+ var view = miniTempWebGLFloatBuffers[3 * count - 1];
+ for (var i = 0; i < 3 * count; i += 3) {
+ view[i] = GROWABLE_HEAP_F32()[value + 4 * i >> 2];
+ view[i + 1] = GROWABLE_HEAP_F32()[value + (4 * i + 4) >> 2];
+ view[i + 2] = GROWABLE_HEAP_F32()[value + (4 * i + 8) >> 2];
+ }
+ } else {
+ var view = GROWABLE_HEAP_F32().subarray(value >> 2, value + count * 12 >> 2);
+ }
+ GLctx.uniform3fv(webglGetUniformLocation(location), view);
+}
+
+function _emscripten_glUniform4f(location, v0, v1, v2, v3) {
+ GLctx.uniform4f(webglGetUniformLocation(location), v0, v1, v2, v3);
+}
+
+function _emscripten_glUniform4fv(location, count, value) {
+ if (GL.currentContext.version >= 2) {
+ GLctx.uniform4fv(webglGetUniformLocation(location), GROWABLE_HEAP_F32(), value >> 2, count * 4);
+ return;
+ }
+ if (count <= 72) {
+ var view = miniTempWebGLFloatBuffers[4 * count - 1];
+ var heap = GROWABLE_HEAP_F32();
+ value >>= 2;
+ for (var i = 0; i < 4 * count; i += 4) {
+ var dst = value + i;
+ view[i] = heap[dst];
+ view[i + 1] = heap[dst + 1];
+ view[i + 2] = heap[dst + 2];
+ view[i + 3] = heap[dst + 3];
+ }
+ } else {
+ var view = GROWABLE_HEAP_F32().subarray(value >> 2, value + count * 16 >> 2);
+ }
+ GLctx.uniform4fv(webglGetUniformLocation(location), view);
+}
+
+function _emscripten_glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding) {
+ program = GL.programs[program];
+ GLctx["uniformBlockBinding"](program, uniformBlockIndex, uniformBlockBinding);
+}
+
+function _emscripten_glUniformMatrix4fv(location, count, transpose, value) {
+ if (GL.currentContext.version >= 2) {
+ GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, GROWABLE_HEAP_F32(), value >> 2, count * 16);
+ return;
+ }
+ if (count <= 18) {
+ var view = miniTempWebGLFloatBuffers[16 * count - 1];
+ var heap = GROWABLE_HEAP_F32();
+ value >>= 2;
+ for (var i = 0; i < 16 * count; i += 16) {
+ var dst = value + i;
+ view[i] = heap[dst];
+ view[i + 1] = heap[dst + 1];
+ view[i + 2] = heap[dst + 2];
+ view[i + 3] = heap[dst + 3];
+ view[i + 4] = heap[dst + 4];
+ view[i + 5] = heap[dst + 5];
+ view[i + 6] = heap[dst + 6];
+ view[i + 7] = heap[dst + 7];
+ view[i + 8] = heap[dst + 8];
+ view[i + 9] = heap[dst + 9];
+ view[i + 10] = heap[dst + 10];
+ view[i + 11] = heap[dst + 11];
+ view[i + 12] = heap[dst + 12];
+ view[i + 13] = heap[dst + 13];
+ view[i + 14] = heap[dst + 14];
+ view[i + 15] = heap[dst + 15];
+ }
+ } else {
+ var view = GROWABLE_HEAP_F32().subarray(value >> 2, value + count * 64 >> 2);
+ }
+ GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, view);
+}
+
+function _emscripten_glUseProgram(program) {
+ program = GL.programs[program];
+ GLctx.useProgram(program);
+ GLctx.currentProgram = program;
+}
+
+function _emscripten_glVertexAttrib4f(x0, x1, x2, x3, x4) {
+ GLctx["vertexAttrib4f"](x0, x1, x2, x3, x4);
+}
+
+function _emscripten_glVertexAttribDivisor(index, divisor) {
+ GLctx["vertexAttribDivisor"](index, divisor);
+}
+
+function _emscripten_glVertexAttribI4ui(x0, x1, x2, x3, x4) {
+ GLctx["vertexAttribI4ui"](x0, x1, x2, x3, x4);
+}
+
+function _emscripten_glVertexAttribIPointer(index, size, type, stride, ptr) {
+ GLctx["vertexAttribIPointer"](index, size, type, stride, ptr);
+}
+
+function _emscripten_glVertexAttribPointer(index, size, type, normalized, stride, ptr) {
+ GLctx.vertexAttribPointer(index, size, type, !!normalized, stride, ptr);
+}
+
+function _emscripten_glViewport(x0, x1, x2, x3) {
+ GLctx["viewport"](x0, x1, x2, x3);
+}
+
+function _emscripten_memcpy_big(dest, src, num) {
+ GROWABLE_HEAP_U8().copyWithin(dest, src, src + num);
+}
+
+function _emscripten_num_logical_cores() {
+ return navigator["hardwareConcurrency"];
+}
+
+function _emscripten_proxy_to_main_thread_js(index, sync) {
+ var numCallArgs = arguments.length - 2;
+ var outerArgs = arguments;
+ if (numCallArgs > 20 - 1) throw "emscripten_proxy_to_main_thread_js: Too many arguments " + numCallArgs + " to proxied function idx=" + index + ", maximum supported is " + (20 - 1) + "!";
+ return withStackSave(function() {
+ var serializedNumCallArgs = numCallArgs;
+ var args = stackAlloc(serializedNumCallArgs * 8);
+ var b = args >> 3;
+ for (var i = 0; i < numCallArgs; i++) {
+ var arg = outerArgs[2 + i];
+ GROWABLE_HEAP_F64()[b + i] = arg;
+ }
+ return _emscripten_run_in_main_runtime_thread_js(index, serializedNumCallArgs, args, sync);
+ });
+}
+
+var _emscripten_receive_on_main_thread_js_callArgs = [];
+
+function _emscripten_receive_on_main_thread_js(index, numCallArgs, args) {
+ _emscripten_receive_on_main_thread_js_callArgs.length = numCallArgs;
+ var b = args >> 3;
+ for (var i = 0; i < numCallArgs; i++) {
+ _emscripten_receive_on_main_thread_js_callArgs[i] = GROWABLE_HEAP_F64()[b + i];
+ }
+ var isEmAsmConst = index < 0;
+ var func = !isEmAsmConst ? proxiedFunctionTable[index] : ASM_CONSTS[-index - 1];
+ assert(func.length == numCallArgs, "Call args mismatch in emscripten_receive_on_main_thread_js");
+ return func.apply(null, _emscripten_receive_on_main_thread_js_callArgs);
+}
+
+function _emscripten_get_heap_max() {
+ return 2147483648;
+}
+
+function emscripten_realloc_buffer(size) {
+ try {
+ wasmMemory.grow(size - buffer.byteLength + 65535 >>> 16);
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ return 1;
+ } catch (e) {
+ err("emscripten_realloc_buffer: Attempted to grow heap from " + buffer.byteLength + " bytes to " + size + " bytes, but got error: " + e);
+ }
+}
+
+function _emscripten_resize_heap(requestedSize) {
+ var oldSize = GROWABLE_HEAP_U8().length;
+ requestedSize = requestedSize >>> 0;
+ if (requestedSize <= oldSize) {
+ return false;
+ }
+ var maxHeapSize = _emscripten_get_heap_max();
+ if (requestedSize > maxHeapSize) {
+ err("Cannot enlarge memory, asked to go up to " + requestedSize + " bytes, but the limit is " + maxHeapSize + " bytes!");
+ return false;
+ }
+ for (var cutDown = 1; cutDown <= 4; cutDown *= 2) {
+ var overGrownHeapSize = oldSize * (1 + .2 / cutDown);
+ overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296);
+ var newSize = Math.min(maxHeapSize, alignUp(Math.max(requestedSize, overGrownHeapSize), 65536));
+ var replacement = emscripten_realloc_buffer(newSize);
+ if (replacement) {
+ return true;
+ }
+ }
+ err("Failed to grow the heap from " + oldSize + " bytes to " + newSize + " bytes, not enough memory!");
+ return false;
+}
+
+var JSEvents = {
+ inEventHandler: 0,
+ removeAllEventListeners: function() {
+ for (var i = JSEvents.eventHandlers.length - 1; i >= 0; --i) {
+ JSEvents._removeHandler(i);
+ }
+ JSEvents.eventHandlers = [];
+ JSEvents.deferredCalls = [];
+ },
+ registerRemoveEventListeners: function() {
+ if (!JSEvents.removeEventListenersRegistered) {
+ __ATEXIT__.push(JSEvents.removeAllEventListeners);
+ JSEvents.removeEventListenersRegistered = true;
+ }
+ },
+ deferredCalls: [],
+ deferCall: function(targetFunction, precedence, argsList) {
+ function arraysHaveEqualContent(arrA, arrB) {
+ if (arrA.length != arrB.length) return false;
+ for (var i in arrA) {
+ if (arrA[i] != arrB[i]) return false;
+ }
+ return true;
+ }
+ for (var i in JSEvents.deferredCalls) {
+ var call = JSEvents.deferredCalls[i];
+ if (call.targetFunction == targetFunction && arraysHaveEqualContent(call.argsList, argsList)) {
+ return;
+ }
+ }
+ JSEvents.deferredCalls.push({
+ targetFunction: targetFunction,
+ precedence: precedence,
+ argsList: argsList
+ });
+ JSEvents.deferredCalls.sort(function(x, y) {
+ return x.precedence < y.precedence;
+ });
+ },
+ removeDeferredCalls: function(targetFunction) {
+ for (var i = 0; i < JSEvents.deferredCalls.length; ++i) {
+ if (JSEvents.deferredCalls[i].targetFunction == targetFunction) {
+ JSEvents.deferredCalls.splice(i, 1);
+ --i;
+ }
+ }
+ },
+ canPerformEventHandlerRequests: function() {
+ return JSEvents.inEventHandler && JSEvents.currentEventHandler.allowsDeferredCalls;
+ },
+ runDeferredCalls: function() {
+ if (!JSEvents.canPerformEventHandlerRequests()) {
+ return;
+ }
+ for (var i = 0; i < JSEvents.deferredCalls.length; ++i) {
+ var call = JSEvents.deferredCalls[i];
+ JSEvents.deferredCalls.splice(i, 1);
+ --i;
+ call.targetFunction.apply(null, call.argsList);
+ }
+ },
+ eventHandlers: [],
+ removeAllHandlersOnTarget: function(target, eventTypeString) {
+ for (var i = 0; i < JSEvents.eventHandlers.length; ++i) {
+ if (JSEvents.eventHandlers[i].target == target && (!eventTypeString || eventTypeString == JSEvents.eventHandlers[i].eventTypeString)) {
+ JSEvents._removeHandler(i--);
+ }
+ }
+ },
+ _removeHandler: function(i) {
+ var h = JSEvents.eventHandlers[i];
+ h.target.removeEventListener(h.eventTypeString, h.eventListenerFunc, h.useCapture);
+ JSEvents.eventHandlers.splice(i, 1);
+ },
+ registerOrRemoveHandler: function(eventHandler) {
+ var jsEventHandler = function jsEventHandler(event) {
+ ++JSEvents.inEventHandler;
+ JSEvents.currentEventHandler = eventHandler;
+ JSEvents.runDeferredCalls();
+ eventHandler.handlerFunc(event);
+ JSEvents.runDeferredCalls();
+ --JSEvents.inEventHandler;
+ };
+ if (eventHandler.callbackfunc) {
+ eventHandler.eventListenerFunc = jsEventHandler;
+ eventHandler.target.addEventListener(eventHandler.eventTypeString, jsEventHandler, eventHandler.useCapture);
+ JSEvents.eventHandlers.push(eventHandler);
+ JSEvents.registerRemoveEventListeners();
+ } else {
+ for (var i = 0; i < JSEvents.eventHandlers.length; ++i) {
+ if (JSEvents.eventHandlers[i].target == eventHandler.target && JSEvents.eventHandlers[i].eventTypeString == eventHandler.eventTypeString) {
+ JSEvents._removeHandler(i--);
+ }
+ }
+ }
+ },
+ queueEventHandlerOnThread_iiii: function(targetThread, eventHandlerFunc, eventTypeId, eventData, userData) {
+ withStackSave(function() {
+ var varargs = stackAlloc(12);
+ GROWABLE_HEAP_I32()[varargs >> 2] = eventTypeId;
+ GROWABLE_HEAP_I32()[varargs + 4 >> 2] = eventData;
+ GROWABLE_HEAP_I32()[varargs + 8 >> 2] = userData;
+ _emscripten_dispatch_to_thread_(targetThread, 637534208, eventHandlerFunc, eventData, varargs);
+ });
+ },
+ getTargetThreadForEventCallback: function(targetThread) {
+ switch (targetThread) {
+ case 1:
+ return 0;
+
+ case 2:
+ return PThread.currentProxiedOperationCallerThread;
+
+ default:
+ return targetThread;
+ }
+ },
+ getNodeNameForTarget: function(target) {
+ if (!target) return "";
+ if (target == window) return "#window";
+ if (target == screen) return "#screen";
+ return target && target.nodeName ? target.nodeName : "";
+ },
+ fullscreenEnabled: function() {
+ return document.fullscreenEnabled || document.webkitFullscreenEnabled;
+ }
+};
+
+function _emscripten_set_offscreencanvas_size_on_target_thread_js(targetThread, targetCanvas, width, height) {
+ withStackSave(function() {
+ var varargs = stackAlloc(12);
+ var targetCanvasPtr = 0;
+ if (targetCanvas) {
+ targetCanvasPtr = stringToNewUTF8(targetCanvas);
+ }
+ GROWABLE_HEAP_I32()[varargs >> 2] = targetCanvasPtr;
+ GROWABLE_HEAP_I32()[varargs + 4 >> 2] = width;
+ GROWABLE_HEAP_I32()[varargs + 8 >> 2] = height;
+ _emscripten_dispatch_to_thread_(targetThread, 657457152, 0, targetCanvasPtr, varargs);
+ });
+}
+
+function _emscripten_set_offscreencanvas_size_on_target_thread(targetThread, targetCanvas, width, height) {
+ targetCanvas = targetCanvas ? UTF8ToString(targetCanvas) : "";
+ _emscripten_set_offscreencanvas_size_on_target_thread_js(targetThread, targetCanvas, width, height);
+}
+
+function maybeCStringToJsString(cString) {
+ return cString > 2 ? UTF8ToString(cString) : cString;
+}
+
+var specialHTMLTargets = [ 0, typeof document != "undefined" ? document : 0, typeof window != "undefined" ? window : 0 ];
+
+function findEventTarget(target) {
+ target = maybeCStringToJsString(target);
+ var domElement = specialHTMLTargets[target] || (typeof document != "undefined" ? document.querySelector(target) : undefined);
+ return domElement;
+}
+
+function findCanvasEventTarget(target) {
+ return findEventTarget(target);
+}
+
+function _emscripten_set_canvas_element_size_calling_thread(target, width, height) {
+ var canvas = findCanvasEventTarget(target);
+ if (!canvas) return -4;
+ if (canvas.canvasSharedPtr) {
+ GROWABLE_HEAP_I32()[canvas.canvasSharedPtr >> 2] = width;
+ GROWABLE_HEAP_I32()[canvas.canvasSharedPtr + 4 >> 2] = height;
+ }
+ if (canvas.offscreenCanvas || !canvas.controlTransferredOffscreen) {
+ if (canvas.offscreenCanvas) canvas = canvas.offscreenCanvas;
+ var autoResizeViewport = false;
+ if (canvas.GLctxObject && canvas.GLctxObject.GLctx) {
+ var prevViewport = canvas.GLctxObject.GLctx.getParameter(2978);
+ autoResizeViewport = prevViewport[0] === 0 && prevViewport[1] === 0 && prevViewport[2] === canvas.width && prevViewport[3] === canvas.height;
+ }
+ canvas.width = width;
+ canvas.height = height;
+ if (autoResizeViewport) {
+ canvas.GLctxObject.GLctx.viewport(0, 0, width, height);
+ }
+ } else if (canvas.canvasSharedPtr) {
+ var targetThread = GROWABLE_HEAP_I32()[canvas.canvasSharedPtr + 8 >> 2];
+ _emscripten_set_offscreencanvas_size_on_target_thread(targetThread, target, width, height);
+ return 1;
+ } else {
+ return -4;
+ }
+ if (canvas.GLctxObject) GL.resizeOffscreenFramebuffer(canvas.GLctxObject);
+ return 0;
+}
+
+function _emscripten_set_canvas_element_size_main_thread(target, width, height) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(34, 1, target, width, height);
+ return _emscripten_set_canvas_element_size_calling_thread(target, width, height);
+}
+
+function _emscripten_set_canvas_element_size(target, width, height) {
+ var canvas = findCanvasEventTarget(target);
+ if (canvas) {
+ return _emscripten_set_canvas_element_size_calling_thread(target, width, height);
+ } else {
+ return _emscripten_set_canvas_element_size_main_thread(target, width, height);
+ }
+}
+
+function _emscripten_set_main_loop(func, fps, simulateInfiniteLoop) {
+ var browserIterationFunc = getWasmTableEntry(func);
+ setMainLoop(browserIterationFunc, fps, simulateInfiniteLoop);
+}
+
+function _emscripten_supports_offscreencanvas() {
+ return 0;
+}
+
+function _emscripten_unwind_to_js_event_loop() {
+ throw "unwind";
+}
+
+function _emscripten_webgl_destroy_context(contextHandle) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(35, 1, contextHandle);
+ if (GL.currentContext == contextHandle) GL.currentContext = 0;
+ GL.deleteContext(contextHandle);
+}
+
+function _emscripten_webgl_do_commit_frame() {
+ if (!GL.currentContext || !GL.currentContext.GLctx) {
+ return -3;
+ }
+ if (GL.currentContext.defaultFbo) {
+ GL.blitOffscreenFramebuffer(GL.currentContext);
+ return 0;
+ }
+ if (!GL.currentContext.attributes.explicitSwapControl) {
+ return -3;
+ }
+ return 0;
+}
+
+var __emscripten_webgl_power_preferences = [ "default", "low-power", "high-performance" ];
+
+function _emscripten_webgl_do_create_context(target, attributes) {
+ assert(attributes);
+ var a = attributes >> 2;
+ var powerPreference = GROWABLE_HEAP_I32()[a + (24 >> 2)];
+ var contextAttributes = {
+ "alpha": !!GROWABLE_HEAP_I32()[a + (0 >> 2)],
+ "depth": !!GROWABLE_HEAP_I32()[a + (4 >> 2)],
+ "stencil": !!GROWABLE_HEAP_I32()[a + (8 >> 2)],
+ "antialias": !!GROWABLE_HEAP_I32()[a + (12 >> 2)],
+ "premultipliedAlpha": !!GROWABLE_HEAP_I32()[a + (16 >> 2)],
+ "preserveDrawingBuffer": !!GROWABLE_HEAP_I32()[a + (20 >> 2)],
+ "powerPreference": __emscripten_webgl_power_preferences[powerPreference],
+ "failIfMajorPerformanceCaveat": !!GROWABLE_HEAP_I32()[a + (28 >> 2)],
+ majorVersion: GROWABLE_HEAP_I32()[a + (32 >> 2)],
+ minorVersion: GROWABLE_HEAP_I32()[a + (36 >> 2)],
+ enableExtensionsByDefault: GROWABLE_HEAP_I32()[a + (40 >> 2)],
+ explicitSwapControl: GROWABLE_HEAP_I32()[a + (44 >> 2)],
+ proxyContextToMainThread: GROWABLE_HEAP_I32()[a + (48 >> 2)],
+ renderViaOffscreenBackBuffer: GROWABLE_HEAP_I32()[a + (52 >> 2)]
+ };
+ var canvas = findCanvasEventTarget(target);
+ if (ENVIRONMENT_IS_PTHREAD) {
+ if (contextAttributes.proxyContextToMainThread === 2 || !canvas && contextAttributes.proxyContextToMainThread === 1) {
+ if (typeof OffscreenCanvas == "undefined") {
+ GROWABLE_HEAP_I32()[attributes + 52 >> 2] = 1;
+ GROWABLE_HEAP_I32()[attributes + 20 >> 2] = 1;
+ }
+ return _emscripten_sync_run_in_main_thread_2(622854144, target, attributes);
+ }
+ }
+ if (!canvas) {
+ return 0;
+ }
+ if (contextAttributes.explicitSwapControl && !contextAttributes.renderViaOffscreenBackBuffer) {
+ contextAttributes.renderViaOffscreenBackBuffer = true;
+ }
+ var contextHandle = GL.createContext(canvas, contextAttributes);
+ return contextHandle;
+}
+
+function _emscripten_webgl_enable_extension(contextHandle, extension) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(36, 1, contextHandle, extension);
+ var context = GL.getContext(contextHandle);
+ var extString = UTF8ToString(extension);
+ if (extString.startsWith("GL_")) extString = extString.substr(3);
+ if (extString == "ANGLE_instanced_arrays") __webgl_enable_ANGLE_instanced_arrays(GLctx);
+ if (extString == "OES_vertex_array_object") __webgl_enable_OES_vertex_array_object(GLctx);
+ if (extString == "WEBGL_draw_buffers") __webgl_enable_WEBGL_draw_buffers(GLctx);
+ if (extString == "WEBGL_draw_instanced_base_vertex_base_instance") __webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);
+ if (extString == "WEBGL_multi_draw_instanced_base_vertex_base_instance") __webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);
+ if (extString == "WEBGL_multi_draw") __webgl_enable_WEBGL_multi_draw(GLctx);
+ var ext = context.GLctx.getExtension(extString);
+ return !!ext;
+}
+
+function _emscripten_webgl_init_context_attributes(attributes) {
+ assert(attributes);
+ var a = attributes >> 2;
+ for (var i = 0; i < 56 >> 2; ++i) {
+ GROWABLE_HEAP_I32()[a + i] = 0;
+ }
+ GROWABLE_HEAP_I32()[a + (0 >> 2)] = GROWABLE_HEAP_I32()[a + (4 >> 2)] = GROWABLE_HEAP_I32()[a + (12 >> 2)] = GROWABLE_HEAP_I32()[a + (16 >> 2)] = GROWABLE_HEAP_I32()[a + (32 >> 2)] = GROWABLE_HEAP_I32()[a + (40 >> 2)] = 1;
+ if (ENVIRONMENT_IS_WORKER) GROWABLE_HEAP_I32()[attributes + 48 >> 2] = 1;
+}
+
+function _emscripten_webgl_make_context_current_calling_thread(contextHandle) {
+ var success = GL.makeContextCurrent(contextHandle);
+ if (success) GL.currentContextIsProxied = false;
+ return success ? 0 : -5;
+}
+
+var ENV = {};
+
+function getExecutableName() {
+ return thisProgram || "./this.program";
+}
+
+function getEnvStrings() {
+ if (!getEnvStrings.strings) {
+ var lang = (typeof navigator == "object" && navigator.languages && navigator.languages[0] || "C").replace("-", "_") + ".UTF-8";
+ var env = {
+ "USER": "web_user",
+ "LOGNAME": "web_user",
+ "PATH": "/",
+ "PWD": "/",
+ "HOME": "/home/web_user",
+ "LANG": lang,
+ "_": getExecutableName()
+ };
+ for (var x in ENV) {
+ if (ENV[x] === undefined) delete env[x]; else env[x] = ENV[x];
+ }
+ var strings = [];
+ for (var x in env) {
+ strings.push(x + "=" + env[x]);
+ }
+ getEnvStrings.strings = strings;
+ }
+ return getEnvStrings.strings;
+}
+
+function _environ_get(__environ, environ_buf) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(37, 1, __environ, environ_buf);
+ var bufSize = 0;
+ getEnvStrings().forEach(function(string, i) {
+ var ptr = environ_buf + bufSize;
+ GROWABLE_HEAP_I32()[__environ + i * 4 >> 2] = ptr;
+ writeAsciiToMemory(string, ptr);
+ bufSize += string.length + 1;
+ });
+ return 0;
+}
+
+function _environ_sizes_get(penviron_count, penviron_buf_size) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(38, 1, penviron_count, penviron_buf_size);
+ var strings = getEnvStrings();
+ GROWABLE_HEAP_I32()[penviron_count >> 2] = strings.length;
+ var bufSize = 0;
+ strings.forEach(function(string) {
+ bufSize += string.length + 1;
+ });
+ GROWABLE_HEAP_I32()[penviron_buf_size >> 2] = bufSize;
+ return 0;
+}
+
+function _fd_close(fd) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(39, 1, fd);
+ try {
+ var stream = SYSCALLS.getStreamFromFD(fd);
+ FS.close(stream);
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return e.errno;
+ }
+}
+
+function _fd_fdstat_get(fd, pbuf) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(40, 1, fd, pbuf);
+ try {
+ var stream = SYSCALLS.getStreamFromFD(fd);
+ var type = stream.tty ? 2 : FS.isDir(stream.mode) ? 3 : FS.isLink(stream.mode) ? 7 : 4;
+ GROWABLE_HEAP_I8()[pbuf >> 0] = type;
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return e.errno;
+ }
+}
+
+function _fd_read(fd, iov, iovcnt, pnum) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(41, 1, fd, iov, iovcnt, pnum);
+ try {
+ var stream = SYSCALLS.getStreamFromFD(fd);
+ var num = SYSCALLS.doReadv(stream, iov, iovcnt);
+ GROWABLE_HEAP_I32()[pnum >> 2] = num;
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return e.errno;
+ }
+}
+
+function _fd_seek(fd, offset_low, offset_high, whence, newOffset) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(42, 1, fd, offset_low, offset_high, whence, newOffset);
+ try {
+ var stream = SYSCALLS.getStreamFromFD(fd);
+ var HIGH_OFFSET = 4294967296;
+ var offset = offset_high * HIGH_OFFSET + (offset_low >>> 0);
+ var DOUBLE_LIMIT = 9007199254740992;
+ if (offset <= -DOUBLE_LIMIT || offset >= DOUBLE_LIMIT) {
+ return -61;
+ }
+ FS.llseek(stream, offset, whence);
+ tempI64 = [ stream.position >>> 0, (tempDouble = stream.position, +Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math.min(+Math.floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ],
+ GROWABLE_HEAP_I32()[newOffset >> 2] = tempI64[0], GROWABLE_HEAP_I32()[newOffset + 4 >> 2] = tempI64[1];
+ if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null;
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return e.errno;
+ }
+}
+
+function _fd_write(fd, iov, iovcnt, pnum) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(43, 1, fd, iov, iovcnt, pnum);
+ try {
+ var stream = SYSCALLS.getStreamFromFD(fd);
+ var num = SYSCALLS.doWritev(stream, iov, iovcnt);
+ GROWABLE_HEAP_I32()[pnum >> 2] = num;
+ return 0;
+ } catch (e) {
+ if (typeof FS == "undefined" || !(e instanceof FS.ErrnoError)) throw e;
+ return e.errno;
+ }
+}
+
+function _getTempRet0() {
+ return getTempRet0();
+}
+
+function _getaddrinfo(node, service, hint, out) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(44, 1, node, service, hint, out);
+ var addrs = [];
+ var canon = null;
+ var addr = 0;
+ var port = 0;
+ var flags = 0;
+ var family = 0;
+ var type = 0;
+ var proto = 0;
+ var ai, last;
+ function allocaddrinfo(family, type, proto, canon, addr, port) {
+ var sa, salen, ai;
+ var errno;
+ salen = family === 10 ? 28 : 16;
+ addr = family === 10 ? inetNtop6(addr) : inetNtop4(addr);
+ sa = _malloc(salen);
+ errno = writeSockaddr(sa, family, addr, port);
+ assert(!errno);
+ ai = _malloc(32);
+ GROWABLE_HEAP_I32()[ai + 4 >> 2] = family;
+ GROWABLE_HEAP_I32()[ai + 8 >> 2] = type;
+ GROWABLE_HEAP_I32()[ai + 12 >> 2] = proto;
+ GROWABLE_HEAP_I32()[ai + 24 >> 2] = canon;
+ GROWABLE_HEAP_I32()[ai + 20 >> 2] = sa;
+ if (family === 10) {
+ GROWABLE_HEAP_I32()[ai + 16 >> 2] = 28;
+ } else {
+ GROWABLE_HEAP_I32()[ai + 16 >> 2] = 16;
+ }
+ GROWABLE_HEAP_I32()[ai + 28 >> 2] = 0;
+ return ai;
+ }
+ if (hint) {
+ flags = GROWABLE_HEAP_I32()[hint >> 2];
+ family = GROWABLE_HEAP_I32()[hint + 4 >> 2];
+ type = GROWABLE_HEAP_I32()[hint + 8 >> 2];
+ proto = GROWABLE_HEAP_I32()[hint + 12 >> 2];
+ }
+ if (type && !proto) {
+ proto = type === 2 ? 17 : 6;
+ }
+ if (!type && proto) {
+ type = proto === 17 ? 2 : 1;
+ }
+ if (proto === 0) {
+ proto = 6;
+ }
+ if (type === 0) {
+ type = 1;
+ }
+ if (!node && !service) {
+ return -2;
+ }
+ if (flags & ~(1 | 2 | 4 | 1024 | 8 | 16 | 32)) {
+ return -1;
+ }
+ if (hint !== 0 && GROWABLE_HEAP_I32()[hint >> 2] & 2 && !node) {
+ return -1;
+ }
+ if (flags & 32) {
+ return -2;
+ }
+ if (type !== 0 && type !== 1 && type !== 2) {
+ return -7;
+ }
+ if (family !== 0 && family !== 2 && family !== 10) {
+ return -6;
+ }
+ if (service) {
+ service = UTF8ToString(service);
+ port = parseInt(service, 10);
+ if (isNaN(port)) {
+ if (flags & 1024) {
+ return -2;
+ }
+ return -8;
+ }
+ }
+ if (!node) {
+ if (family === 0) {
+ family = 2;
+ }
+ if ((flags & 1) === 0) {
+ if (family === 2) {
+ addr = _htonl(2130706433);
+ } else {
+ addr = [ 0, 0, 0, 1 ];
+ }
+ }
+ ai = allocaddrinfo(family, type, proto, null, addr, port);
+ GROWABLE_HEAP_I32()[out >> 2] = ai;
+ return 0;
+ }
+ node = UTF8ToString(node);
+ addr = inetPton4(node);
+ if (addr !== null) {
+ if (family === 0 || family === 2) {
+ family = 2;
+ } else if (family === 10 && flags & 8) {
+ addr = [ 0, 0, _htonl(65535), addr ];
+ family = 10;
+ } else {
+ return -2;
+ }
+ } else {
+ addr = inetPton6(node);
+ if (addr !== null) {
+ if (family === 0 || family === 10) {
+ family = 10;
+ } else {
+ return -2;
+ }
+ }
+ }
+ if (addr != null) {
+ ai = allocaddrinfo(family, type, proto, node, addr, port);
+ GROWABLE_HEAP_I32()[out >> 2] = ai;
+ return 0;
+ }
+ if (flags & 4) {
+ return -2;
+ }
+ node = DNS.lookup_name(node);
+ addr = inetPton4(node);
+ if (family === 0) {
+ family = 2;
+ } else if (family === 10) {
+ addr = [ 0, 0, _htonl(65535), addr ];
+ }
+ ai = allocaddrinfo(family, type, proto, null, addr, port);
+ GROWABLE_HEAP_I32()[out >> 2] = ai;
+ return 0;
+}
+
+function _getnameinfo(sa, salen, node, nodelen, serv, servlen, flags) {
+ var info = readSockaddr(sa, salen);
+ if (info.errno) {
+ return -6;
+ }
+ var port = info.port;
+ var addr = info.addr;
+ var overflowed = false;
+ if (node && nodelen) {
+ var lookup;
+ if (flags & 1 || !(lookup = DNS.lookup_addr(addr))) {
+ if (flags & 8) {
+ return -2;
+ }
+ } else {
+ addr = lookup;
+ }
+ var numBytesWrittenExclNull = stringToUTF8(addr, node, nodelen);
+ if (numBytesWrittenExclNull + 1 >= nodelen) {
+ overflowed = true;
+ }
+ }
+ if (serv && servlen) {
+ port = "" + port;
+ var numBytesWrittenExclNull = stringToUTF8(port, serv, servlen);
+ if (numBytesWrittenExclNull + 1 >= servlen) {
+ overflowed = true;
+ }
+ }
+ if (overflowed) {
+ return -12;
+ }
+ return 0;
+}
+
+function _gettimeofday(ptr) {
+ var now = Date.now();
+ GROWABLE_HEAP_I32()[ptr >> 2] = now / 1e3 | 0;
+ GROWABLE_HEAP_I32()[ptr + 4 >> 2] = now % 1e3 * 1e3 | 0;
+ return 0;
+}
+
+var GodotRuntime = {
+ get_func: function(ptr) {
+ return wasmTable.get(ptr);
+ },
+ error: function() {
+ err.apply(null, Array.from(arguments));
+ },
+ print: function() {
+ out.apply(null, Array.from(arguments));
+ },
+ malloc: function(p_size) {
+ return _malloc(p_size);
+ },
+ free: function(p_ptr) {
+ _free(p_ptr);
+ },
+ getHeapValue: function(p_ptr, p_type) {
+ return getValue(p_ptr, p_type);
+ },
+ setHeapValue: function(p_ptr, p_value, p_type) {
+ setValue(p_ptr, p_value, p_type);
+ },
+ heapSub: function(p_heap, p_ptr, p_len) {
+ const bytes = p_heap.BYTES_PER_ELEMENT;
+ return p_heap.subarray(p_ptr / bytes, p_ptr / bytes + p_len);
+ },
+ heapSlice: function(p_heap, p_ptr, p_len) {
+ const bytes = p_heap.BYTES_PER_ELEMENT;
+ return p_heap.slice(p_ptr / bytes, p_ptr / bytes + p_len);
+ },
+ heapCopy: function(p_dst, p_src, p_ptr) {
+ const bytes = p_src.BYTES_PER_ELEMENT;
+ return p_dst.set(p_src, p_ptr / bytes);
+ },
+ parseString: function(p_ptr) {
+ return UTF8ToString(p_ptr);
+ },
+ parseStringArray: function(p_ptr, p_size) {
+ const strings = [];
+ const ptrs = GodotRuntime.heapSub(GROWABLE_HEAP_I32(), p_ptr, p_size);
+ ptrs.forEach(function(ptr) {
+ strings.push(GodotRuntime.parseString(ptr));
+ });
+ return strings;
+ },
+ strlen: function(p_str) {
+ return lengthBytesUTF8(p_str);
+ },
+ allocString: function(p_str) {
+ const length = GodotRuntime.strlen(p_str) + 1;
+ const c_str = GodotRuntime.malloc(length);
+ stringToUTF8(p_str, c_str, length);
+ return c_str;
+ },
+ allocStringArray: function(p_strings) {
+ const size = p_strings.length;
+ const c_ptr = GodotRuntime.malloc(size * 4);
+ for (let i = 0; i < size; i++) {
+ GROWABLE_HEAP_I32()[(c_ptr >> 2) + i] = GodotRuntime.allocString(p_strings[i]);
+ }
+ return c_ptr;
+ },
+ freeStringArray: function(p_ptr, p_len) {
+ for (let i = 0; i < p_len; i++) {
+ GodotRuntime.free(GROWABLE_HEAP_I32()[(p_ptr >> 2) + i]);
+ }
+ GodotRuntime.free(p_ptr);
+ },
+ stringToHeap: function(p_str, p_ptr, p_len) {
+ return stringToUTF8Array(p_str, GROWABLE_HEAP_I8(), p_ptr, p_len);
+ }
+};
+
+var GodotConfig = {
+ canvas: null,
+ locale: "en",
+ canvas_resize_policy: 2,
+ virtual_keyboard: false,
+ persistent_drops: false,
+ on_execute: null,
+ on_exit: null,
+ init_config: function(p_opts) {
+ GodotConfig.canvas_resize_policy = p_opts["canvasResizePolicy"];
+ GodotConfig.canvas = p_opts["canvas"];
+ GodotConfig.locale = p_opts["locale"] || GodotConfig.locale;
+ GodotConfig.virtual_keyboard = p_opts["virtualKeyboard"];
+ GodotConfig.persistent_drops = !!p_opts["persistentDrops"];
+ GodotConfig.on_execute = p_opts["onExecute"];
+ GodotConfig.on_exit = p_opts["onExit"];
+ if (p_opts["focusCanvas"]) {
+ GodotConfig.canvas.focus();
+ }
+ },
+ locate_file: function(file) {
+ return Module["locateFile"](file);
+ },
+ clear: function() {
+ GodotConfig.canvas = null;
+ GodotConfig.locale = "en";
+ GodotConfig.canvas_resize_policy = 2;
+ GodotConfig.virtual_keyboard = false;
+ GodotConfig.persistent_drops = false;
+ GodotConfig.on_execute = null;
+ GodotConfig.on_exit = null;
+ }
+};
+
+var GodotFS = {
+ ENOENT: 44,
+ _idbfs: false,
+ _syncing: false,
+ _mount_points: [],
+ is_persistent: function() {
+ return GodotFS._idbfs ? 1 : 0;
+ },
+ init: function(persistentPaths) {
+ GodotFS._idbfs = false;
+ if (!Array.isArray(persistentPaths)) {
+ return Promise.reject(new Error("Persistent paths must be an array"));
+ }
+ if (!persistentPaths.length) {
+ return Promise.resolve();
+ }
+ GodotFS._mount_points = persistentPaths.slice();
+ function createRecursive(dir) {
+ try {
+ FS.stat(dir);
+ } catch (e) {
+ if (e.errno !== GodotFS.ENOENT) {
+ GodotRuntime.error(e);
+ }
+ FS.mkdirTree(dir);
+ }
+ }
+ GodotFS._mount_points.forEach(function(path) {
+ createRecursive(path);
+ FS.mount(IDBFS, {}, path);
+ });
+ return new Promise(function(resolve, reject) {
+ FS.syncfs(true, function(err) {
+ if (err) {
+ GodotFS._mount_points = [];
+ GodotFS._idbfs = false;
+ GodotRuntime.print(`IndexedDB not available: ${err.message}`);
+ } else {
+ GodotFS._idbfs = true;
+ }
+ resolve(err);
+ });
+ });
+ },
+ deinit: function() {
+ GodotFS._mount_points.forEach(function(path) {
+ try {
+ FS.unmount(path);
+ } catch (e) {
+ GodotRuntime.print("Already unmounted", e);
+ }
+ if (GodotFS._idbfs && IDBFS.dbs[path]) {
+ IDBFS.dbs[path].close();
+ delete IDBFS.dbs[path];
+ }
+ });
+ GodotFS._mount_points = [];
+ GodotFS._idbfs = false;
+ GodotFS._syncing = false;
+ },
+ sync: function() {
+ if (GodotFS._syncing) {
+ GodotRuntime.error("Already syncing!");
+ return Promise.resolve();
+ }
+ GodotFS._syncing = true;
+ return new Promise(function(resolve, reject) {
+ FS.syncfs(false, function(error) {
+ if (error) {
+ GodotRuntime.error(`Failed to save IDB file system: ${error.message}`);
+ }
+ GodotFS._syncing = false;
+ resolve(error);
+ });
+ });
+ },
+ copy_to_fs: function(path, buffer) {
+ const idx = path.lastIndexOf("/");
+ let dir = "/";
+ if (idx > 0) {
+ dir = path.slice(0, idx);
+ }
+ try {
+ FS.stat(dir);
+ } catch (e) {
+ if (e.errno !== GodotFS.ENOENT) {
+ GodotRuntime.error(e);
+ }
+ FS.mkdirTree(dir);
+ }
+ FS.writeFile(path, new Uint8Array(buffer));
+ }
+};
+
+var GodotOS = {
+ request_quit: function() {},
+ _async_cbs: [],
+ _fs_sync_promise: null,
+ atexit: function(p_promise_cb) {
+ GodotOS._async_cbs.push(p_promise_cb);
+ },
+ cleanup: function(exit_code) {
+ const cb = GodotConfig.on_exit;
+ GodotFS.deinit();
+ GodotConfig.clear();
+ if (cb) {
+ cb(exit_code);
+ }
+ },
+ finish_async: function(callback) {
+ GodotOS._fs_sync_promise.then(function(err) {
+ const promises = [];
+ GodotOS._async_cbs.forEach(function(cb) {
+ promises.push(new Promise(cb));
+ });
+ return Promise.all(promises);
+ }).then(function() {
+ return GodotFS.sync();
+ }).then(function(err) {
+ setTimeout(function() {
+ callback();
+ }, 0);
+ });
+ }
+};
+
+var GodotAudio = {
+ ctx: null,
+ input: null,
+ driver: null,
+ interval: 0,
+ init: function(mix_rate, latency, onstatechange, onlatencyupdate) {
+ const opts = {};
+ if (mix_rate) {
+ opts["sampleRate"] = mix_rate;
+ }
+ const ctx = new (window.AudioContext || window.webkitAudioContext)(opts);
+ GodotAudio.ctx = ctx;
+ ctx.onstatechange = function() {
+ let state = 0;
+ switch (ctx.state) {
+ case "suspended":
+ state = 0;
+ break;
+
+ case "running":
+ state = 1;
+ break;
+
+ case "closed":
+ state = 2;
+ break;
+ }
+ onstatechange(state);
+ };
+ ctx.onstatechange();
+ GodotAudio.interval = setInterval(function() {
+ let computed_latency = 0;
+ if (ctx.baseLatency) {
+ computed_latency += GodotAudio.ctx.baseLatency;
+ }
+ if (ctx.outputLatency) {
+ computed_latency += GodotAudio.ctx.outputLatency;
+ }
+ onlatencyupdate(computed_latency);
+ }, 1e3);
+ GodotOS.atexit(GodotAudio.close_async);
+ return ctx.destination.channelCount;
+ },
+ create_input: function(callback) {
+ if (GodotAudio.input) {
+ return 0;
+ }
+ function gotMediaInput(stream) {
+ try {
+ GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream);
+ callback(GodotAudio.input);
+ } catch (e) {
+ GodotRuntime.error("Failed creating input.", e);
+ }
+ }
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+ navigator.mediaDevices.getUserMedia({
+ "audio": true
+ }).then(gotMediaInput, function(e) {
+ GodotRuntime.error("Error getting user media.", e);
+ });
+ } else {
+ if (!navigator.getUserMedia) {
+ navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+ }
+ if (!navigator.getUserMedia) {
+ GodotRuntime.error("getUserMedia not available.");
+ return 1;
+ }
+ navigator.getUserMedia({
+ "audio": true
+ }, gotMediaInput, function(e) {
+ GodotRuntime.print(e);
+ });
+ }
+ return 0;
+ },
+ close_async: function(resolve, reject) {
+ const ctx = GodotAudio.ctx;
+ GodotAudio.ctx = null;
+ if (!ctx) {
+ resolve();
+ return;
+ }
+ if (GodotAudio.interval) {
+ clearInterval(GodotAudio.interval);
+ GodotAudio.interval = 0;
+ }
+ if (GodotAudio.input) {
+ GodotAudio.input.disconnect();
+ GodotAudio.input = null;
+ }
+ let closed = Promise.resolve();
+ if (GodotAudio.driver) {
+ closed = GodotAudio.driver.close();
+ }
+ closed.then(function() {
+ return ctx.close();
+ }).then(function() {
+ ctx.onstatechange = null;
+ resolve();
+ }).catch(function(e) {
+ ctx.onstatechange = null;
+ GodotRuntime.error("Error closing AudioContext", e);
+ resolve();
+ });
+ }
+};
+
+function _godot_audio_has_worklet() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(45, 1);
+ return GodotAudio.ctx && GodotAudio.ctx.audioWorklet ? 1 : 0;
+}
+
+function _godot_audio_init(p_mix_rate, p_latency, p_state_change, p_latency_update) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(46, 1, p_mix_rate, p_latency, p_state_change, p_latency_update);
+ const statechange = GodotRuntime.get_func(p_state_change);
+ const latencyupdate = GodotRuntime.get_func(p_latency_update);
+ const mix_rate = GodotRuntime.getHeapValue(p_mix_rate, "i32");
+ const channels = GodotAudio.init(mix_rate, p_latency, statechange, latencyupdate);
+ GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, "i32");
+ return channels;
+}
+
+function _godot_audio_input_start() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(47, 1);
+ return GodotAudio.create_input(function(input) {
+ input.connect(GodotAudio.driver.get_node());
+ });
+}
+
+function _godot_audio_input_stop() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(48, 1);
+ if (GodotAudio.input) {
+ const tracks = GodotAudio.input["mediaStream"]["getTracks"]();
+ for (let i = 0; i < tracks.length; i++) {
+ tracks[i]["stop"]();
+ }
+ GodotAudio.input.disconnect();
+ GodotAudio.input = null;
+ }
+}
+
+function _godot_audio_is_available() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(49, 1);
+ if (!(window.AudioContext || window.webkitAudioContext)) {
+ return 0;
+ }
+ return 1;
+}
+
+function _godot_audio_resume() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(50, 1);
+ if (GodotAudio.ctx && GodotAudio.ctx.state !== "running") {
+ GodotAudio.ctx.resume();
+ }
+}
+
+var GodotAudioWorklet = {
+ promise: null,
+ worklet: null,
+ ring_buffer: null,
+ create: function(channels) {
+ const path = GodotConfig.locate_file("godot.audio.worklet.js");
+ GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function() {
+ GodotAudioWorklet.worklet = new AudioWorkletNode(GodotAudio.ctx, "godot-processor", {
+ "outputChannelCount": [ channels ]
+ });
+ return Promise.resolve();
+ });
+ GodotAudio.driver = GodotAudioWorklet;
+ },
+ start: function(in_buf, out_buf, state) {
+ GodotAudioWorklet.promise.then(function() {
+ const node = GodotAudioWorklet.worklet;
+ node.connect(GodotAudio.ctx.destination);
+ node.port.postMessage({
+ "cmd": "start",
+ "data": [ state, in_buf, out_buf ]
+ });
+ node.port.onmessage = function(event) {
+ GodotRuntime.error(event.data);
+ };
+ });
+ },
+ start_no_threads: function(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) {
+ function RingBuffer() {
+ let wpos = 0;
+ let rpos = 0;
+ let pending_samples = 0;
+ const wbuf = new Float32Array(p_out_size);
+ function send(port) {
+ if (pending_samples === 0) {
+ return;
+ }
+ const buffer = GodotRuntime.heapSub(GROWABLE_HEAP_F32(), p_out_buf, p_out_size);
+ const size = buffer.length;
+ const tot_sent = pending_samples;
+ out_callback(wpos, pending_samples);
+ if (wpos + pending_samples >= size) {
+ const high = size - wpos;
+ wbuf.set(buffer.subarray(wpos, size));
+ pending_samples -= high;
+ wpos = 0;
+ }
+ if (pending_samples > 0) {
+ wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples);
+ }
+ port.postMessage({
+ "cmd": "chunk",
+ "data": wbuf.subarray(0, tot_sent)
+ });
+ wpos += pending_samples;
+ pending_samples = 0;
+ }
+ this.receive = function(recv_buf) {
+ const buffer = GodotRuntime.heapSub(GROWABLE_HEAP_F32(), p_in_buf, p_in_size);
+ const from = rpos;
+ let to_write = recv_buf.length;
+ let high = 0;
+ if (rpos + to_write >= p_in_size) {
+ high = p_in_size - rpos;
+ buffer.set(recv_buf.subarray(0, high), rpos);
+ to_write -= high;
+ rpos = 0;
+ }
+ if (to_write) {
+ buffer.set(recv_buf.subarray(high, to_write), rpos);
+ }
+ in_callback(from, recv_buf.length);
+ rpos += to_write;
+ };
+ this.consumed = function(size, port) {
+ pending_samples += size;
+ send(port);
+ };
+ }
+ GodotAudioWorklet.ring_buffer = new RingBuffer();
+ GodotAudioWorklet.promise.then(function() {
+ const node = GodotAudioWorklet.worklet;
+ const buffer = GodotRuntime.heapSlice(GROWABLE_HEAP_F32(), p_out_buf, p_out_size);
+ node.connect(GodotAudio.ctx.destination);
+ node.port.postMessage({
+ "cmd": "start_nothreads",
+ "data": [ buffer, p_in_size ]
+ });
+ node.port.onmessage = function(event) {
+ if (!GodotAudioWorklet.worklet) {
+ return;
+ }
+ if (event.data["cmd"] === "read") {
+ const read = event.data["data"];
+ GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port);
+ } else if (event.data["cmd"] === "input") {
+ const buf = event.data["data"];
+ if (buf.length > p_in_size) {
+ GodotRuntime.error("Input chunk is too big");
+ return;
+ }
+ GodotAudioWorklet.ring_buffer.receive(buf);
+ } else {
+ GodotRuntime.error(event.data);
+ }
+ };
+ });
+ },
+ get_node: function() {
+ return GodotAudioWorklet.worklet;
+ },
+ close: function() {
+ return new Promise(function(resolve, reject) {
+ if (GodotAudioWorklet.promise === null) {
+ return;
+ }
+ const p = GodotAudioWorklet.promise;
+ p.then(function() {
+ GodotAudioWorklet.worklet.port.postMessage({
+ "cmd": "stop",
+ "data": null
+ });
+ GodotAudioWorklet.worklet.disconnect();
+ GodotAudioWorklet.worklet.port.onmessage = null;
+ GodotAudioWorklet.worklet = null;
+ GodotAudioWorklet.promise = null;
+ resolve();
+ }).catch(function(err) {
+ GodotRuntime.error(err);
+ });
+ });
+ }
+};
+
+function _godot_audio_worklet_create(channels) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(51, 1, channels);
+ try {
+ GodotAudioWorklet.create(channels);
+ } catch (e) {
+ GodotRuntime.error("Error starting AudioDriverWorklet", e);
+ return 1;
+ }
+ return 0;
+}
+
+function _godot_audio_worklet_start(p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(52, 1, p_in_buf, p_in_size, p_out_buf, p_out_size, p_state);
+ const out_buffer = GodotRuntime.heapSub(GROWABLE_HEAP_F32(), p_out_buf, p_out_size);
+ const in_buffer = GodotRuntime.heapSub(GROWABLE_HEAP_F32(), p_in_buf, p_in_size);
+ const state = GodotRuntime.heapSub(GROWABLE_HEAP_I32(), p_state, 4);
+ GodotAudioWorklet.start(in_buffer, out_buffer, state);
+}
+
+function _godot_audio_worklet_state_add(p_state, p_idx, p_value) {
+ return Atomics.add(GROWABLE_HEAP_I32(), (p_state >> 2) + p_idx, p_value);
+}
+
+function _godot_audio_worklet_state_get(p_state, p_idx) {
+ return Atomics.load(GROWABLE_HEAP_I32(), (p_state >> 2) + p_idx);
+}
+
+function _godot_audio_worklet_state_wait(p_state, p_idx, p_expected, p_timeout) {
+ Atomics.wait(GROWABLE_HEAP_I32(), (p_state >> 2) + p_idx, p_expected, p_timeout);
+ return Atomics.load(GROWABLE_HEAP_I32(), (p_state >> 2) + p_idx);
+}
+
+function _godot_js_config_canvas_id_get(p_ptr, p_ptr_max) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(53, 1, p_ptr, p_ptr_max);
+ GodotRuntime.stringToHeap(`#${GodotConfig.canvas.id}`, p_ptr, p_ptr_max);
+}
+
+function _godot_js_config_locale_get(p_ptr, p_ptr_max) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(54, 1, p_ptr, p_ptr_max);
+ GodotRuntime.stringToHeap(GodotConfig.locale, p_ptr, p_ptr_max);
+}
+
+var GodotDisplayCursor = {
+ shape: "default",
+ visible: true,
+ cursors: {},
+ set_style: function(style) {
+ GodotConfig.canvas.style.cursor = style;
+ },
+ set_shape: function(shape) {
+ GodotDisplayCursor.shape = shape;
+ let css = shape;
+ if (shape in GodotDisplayCursor.cursors) {
+ const c = GodotDisplayCursor.cursors[shape];
+ css = `url("https://github.com/$%7Bc.url%7D") ${c.x} ${c.y}, default`;
+ }
+ if (GodotDisplayCursor.visible) {
+ GodotDisplayCursor.set_style(css);
+ }
+ },
+ clear: function() {
+ GodotDisplayCursor.set_style("");
+ GodotDisplayCursor.shape = "default";
+ GodotDisplayCursor.visible = true;
+ Object.keys(GodotDisplayCursor.cursors).forEach(function(key) {
+ URL.revokeObjectURL(GodotDisplayCursor.cursors[key]);
+ delete GodotDisplayCursor.cursors[key];
+ });
+ },
+ lockPointer: function() {
+ const canvas = GodotConfig.canvas;
+ if (canvas.requestPointerLock) {
+ canvas.requestPointerLock();
+ }
+ },
+ releasePointer: function() {
+ if (document.exitPointerLock) {
+ document.exitPointerLock();
+ }
+ },
+ isPointerLocked: function() {
+ return document.pointerLockElement === GodotConfig.canvas;
+ }
+};
+
+var GodotEventListeners = {
+ handlers: [],
+ has: function(target, event, method, capture) {
+ return GodotEventListeners.handlers.findIndex(function(e) {
+ return e.target === target && e.event === event && e.method === method && e.capture === capture;
+ }) !== -1;
+ },
+ add: function(target, event, method, capture) {
+ if (GodotEventListeners.has(target, event, method, capture)) {
+ return;
+ }
+ function Handler(p_target, p_event, p_method, p_capture) {
+ this.target = p_target;
+ this.event = p_event;
+ this.method = p_method;
+ this.capture = p_capture;
+ }
+ GodotEventListeners.handlers.push(new Handler(target, event, method, capture));
+ target.addEventListener(event, method, capture);
+ },
+ clear: function() {
+ GodotEventListeners.handlers.forEach(function(h) {
+ h.target.removeEventListener(h.event, h.method, h.capture);
+ });
+ GodotEventListeners.handlers.length = 0;
+ }
+};
+
+var GodotDisplayScreen = {
+ desired_size: [ 0, 0 ],
+ hidpi: true,
+ getPixelRatio: function() {
+ return GodotDisplayScreen.hidpi ? window.devicePixelRatio || 1 : 1;
+ },
+ isFullscreen: function() {
+ const elem = document.fullscreenElement || document.mozFullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
+ if (elem) {
+ return elem === GodotConfig.canvas;
+ }
+ return document.fullscreen || document.mozFullScreen || document.webkitIsFullscreen;
+ },
+ hasFullscreen: function() {
+ return document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled;
+ },
+ requestFullscreen: function() {
+ if (!GodotDisplayScreen.hasFullscreen()) {
+ return 1;
+ }
+ const canvas = GodotConfig.canvas;
+ try {
+ const promise = (canvas.requestFullscreen || canvas.msRequestFullscreen || canvas.mozRequestFullScreen || canvas.mozRequestFullscreen || canvas.webkitRequestFullscreen).call(canvas);
+ if (promise) {
+ promise.catch(function() {});
+ }
+ } catch (e) {
+ return 1;
+ }
+ return 0;
+ },
+ exitFullscreen: function() {
+ if (!GodotDisplayScreen.isFullscreen()) {
+ return 0;
+ }
+ try {
+ const promise = document.exitFullscreen();
+ if (promise) {
+ promise.catch(function() {});
+ }
+ } catch (e) {
+ return 1;
+ }
+ return 0;
+ },
+ _updateGL: function() {
+ const gl_context_handle = _emscripten_webgl_get_current_context();
+ const gl = GL.getContext(gl_context_handle);
+ if (gl) {
+ GL.resizeOffscreenFramebuffer(gl);
+ }
+ },
+ updateSize: function() {
+ const isFullscreen = GodotDisplayScreen.isFullscreen();
+ const wantsFullWindow = GodotConfig.canvas_resize_policy === 2;
+ const noResize = GodotConfig.canvas_resize_policy === 0;
+ const dWidth = GodotDisplayScreen.desired_size[0];
+ const dHeight = GodotDisplayScreen.desired_size[1];
+ const canvas = GodotConfig.canvas;
+ let width = dWidth;
+ let height = dHeight;
+ if (noResize) {
+ if (canvas.width !== width || canvas.height !== height) {
+ GodotDisplayScreen.desired_size = [ canvas.width, canvas.height ];
+ GodotDisplayScreen._updateGL();
+ return 1;
+ }
+ return 0;
+ }
+ const scale = GodotDisplayScreen.getPixelRatio();
+ if (isFullscreen || wantsFullWindow) {
+ width = window.innerWidth * scale;
+ height = window.innerHeight * scale;
+ }
+ const csw = `${width / scale}px`;
+ const csh = `${height / scale}px`;
+ if (canvas.style.width !== csw || canvas.style.height !== csh || canvas.width !== width || canvas.height !== height) {
+ canvas.width = width;
+ canvas.height = height;
+ canvas.style.width = csw;
+ canvas.style.height = csh;
+ GodotDisplayScreen._updateGL();
+ return 1;
+ }
+ return 0;
+ }
+};
+
+var GodotDisplayVK = {
+ textinput: null,
+ textarea: null,
+ available: function() {
+ return GodotConfig.virtual_keyboard && "ontouchstart" in window;
+ },
+ init: function(input_cb) {
+ function create(what) {
+ const elem = document.createElement(what);
+ elem.style.display = "none";
+ elem.style.position = "absolute";
+ elem.style.zIndex = "-1";
+ elem.style.background = "transparent";
+ elem.style.padding = "0px";
+ elem.style.margin = "0px";
+ elem.style.overflow = "hidden";
+ elem.style.width = "0px";
+ elem.style.height = "0px";
+ elem.style.border = "0px";
+ elem.style.outline = "none";
+ elem.readonly = true;
+ elem.disabled = true;
+ GodotEventListeners.add(elem, "input", function(evt) {
+ const c_str = GodotRuntime.allocString(elem.value);
+ input_cb(c_str, elem.selectionEnd);
+ GodotRuntime.free(c_str);
+ }, false);
+ GodotEventListeners.add(elem, "blur", function(evt) {
+ elem.style.display = "none";
+ elem.readonly = true;
+ elem.disabled = true;
+ }, false);
+ GodotConfig.canvas.insertAdjacentElement("beforebegin", elem);
+ return elem;
+ }
+ GodotDisplayVK.textinput = create("input");
+ GodotDisplayVK.textarea = create("textarea");
+ GodotDisplayVK.updateSize();
+ },
+ show: function(text, type, start, end) {
+ if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) {
+ return;
+ }
+ if (GodotDisplayVK.textinput.style.display !== "" || GodotDisplayVK.textarea.style.display !== "") {
+ GodotDisplayVK.hide();
+ }
+ GodotDisplayVK.updateSize();
+ let elem = GodotDisplayVK.textinput;
+ switch (type) {
+ case 0:
+ elem.type = "text";
+ elem.inputmode = "";
+ break;
+
+ case 1:
+ elem = GodotDisplayVK.textarea;
+ break;
+
+ case 2:
+ elem.type = "text";
+ elem.inputmode = "numeric";
+ break;
+
+ case 3:
+ elem.type = "text";
+ elem.inputmode = "decimal";
+ break;
+
+ case 4:
+ elem.type = "tel";
+ elem.inputmode = "";
+ break;
+
+ case 5:
+ elem.type = "email";
+ elem.inputmode = "";
+ break;
+
+ case 6:
+ elem.type = "password";
+ elem.inputmode = "";
+ break;
+
+ case 7:
+ elem.type = "url";
+ elem.inputmode = "";
+ break;
+
+ default:
+ elem.type = "text";
+ elem.inputmode = "";
+ break;
+ }
+ elem.readonly = false;
+ elem.disabled = false;
+ elem.value = text;
+ elem.style.display = "block";
+ elem.focus();
+ elem.setSelectionRange(start, end);
+ },
+ hide: function() {
+ if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) {
+ return;
+ }
+ [ GodotDisplayVK.textinput, GodotDisplayVK.textarea ].forEach(function(elem) {
+ elem.blur();
+ elem.style.display = "none";
+ elem.value = "";
+ });
+ },
+ updateSize: function() {
+ if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) {
+ return;
+ }
+ const rect = GodotConfig.canvas.getBoundingClientRect();
+ function update(elem) {
+ elem.style.left = `${rect.left}px`;
+ elem.style.top = `${rect.top}px`;
+ elem.style.width = `${rect.width}px`;
+ elem.style.height = `${rect.height}px`;
+ }
+ update(GodotDisplayVK.textinput);
+ update(GodotDisplayVK.textarea);
+ },
+ clear: function() {
+ if (GodotDisplayVK.textinput) {
+ GodotDisplayVK.textinput.remove();
+ GodotDisplayVK.textinput = null;
+ }
+ if (GodotDisplayVK.textarea) {
+ GodotDisplayVK.textarea.remove();
+ GodotDisplayVK.textarea = null;
+ }
+ }
+};
+
+var GodotDisplay = {
+ window_icon: "",
+ getDPI: function() {
+ const dpi = Math.round(window.devicePixelRatio * 96);
+ return dpi >= 96 ? dpi : 96;
+ }
+};
+
+function _godot_js_display_alert(p_text) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(55, 1, p_text);
+ window.alert(GodotRuntime.parseString(p_text));
+}
+
+function _godot_js_display_canvas_focus() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(56, 1);
+ GodotConfig.canvas.focus();
+}
+
+function _godot_js_display_canvas_is_focused() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(57, 1);
+ return document.activeElement === GodotConfig.canvas;
+}
+
+function _godot_js_display_clipboard_get(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(58, 1, callback);
+ const func = GodotRuntime.get_func(callback);
+ try {
+ navigator.clipboard.readText().then(function(result) {
+ const ptr = GodotRuntime.allocString(result);
+ func(ptr);
+ GodotRuntime.free(ptr);
+ }).catch(function(e) {});
+ } catch (e) {}
+}
+
+function _godot_js_display_clipboard_set(p_text) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(59, 1, p_text);
+ const text = GodotRuntime.parseString(p_text);
+ if (!navigator.clipboard || !navigator.clipboard.writeText) {
+ return 1;
+ }
+ navigator.clipboard.writeText(text).catch(function(e) {
+ GodotRuntime.error("Setting OS clipboard is only possible from an input callback for the Web platform. Exception:", e);
+ });
+ return 0;
+}
+
+function _godot_js_display_cursor_is_hidden() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(60, 1);
+ return !GodotDisplayCursor.visible;
+}
+
+function _godot_js_display_cursor_is_locked() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(61, 1);
+ return GodotDisplayCursor.isPointerLocked() ? 1 : 0;
+}
+
+function _godot_js_display_cursor_lock_set(p_lock) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(62, 1, p_lock);
+ if (p_lock) {
+ GodotDisplayCursor.lockPointer();
+ } else {
+ GodotDisplayCursor.releasePointer();
+ }
+}
+
+function _godot_js_display_cursor_set_custom_shape(p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(63, 1, p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y);
+ const shape = GodotRuntime.parseString(p_shape);
+ const old_shape = GodotDisplayCursor.cursors[shape];
+ if (p_len > 0) {
+ const png = new Blob([ GodotRuntime.heapSlice(GROWABLE_HEAP_U8(), p_ptr, p_len) ], {
+ type: "image/png"
+ });
+ const url = URL.createObjectURL(png);
+ GodotDisplayCursor.cursors[shape] = {
+ url: url,
+ x: p_hotspot_x,
+ y: p_hotspot_y
+ };
+ } else {
+ delete GodotDisplayCursor.cursors[shape];
+ }
+ if (shape === GodotDisplayCursor.shape) {
+ GodotDisplayCursor.set_shape(GodotDisplayCursor.shape);
+ }
+ if (old_shape) {
+ URL.revokeObjectURL(old_shape.url);
+ }
+}
+
+function _godot_js_display_cursor_set_shape(p_string) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(64, 1, p_string);
+ GodotDisplayCursor.set_shape(GodotRuntime.parseString(p_string));
+}
+
+function _godot_js_display_cursor_set_visible(p_visible) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(65, 1, p_visible);
+ const visible = p_visible !== 0;
+ if (visible === GodotDisplayCursor.visible) {
+ return;
+ }
+ GodotDisplayCursor.visible = visible;
+ if (visible) {
+ GodotDisplayCursor.set_shape(GodotDisplayCursor.shape);
+ } else {
+ GodotDisplayCursor.set_style("none");
+ }
+}
+
+function _godot_js_display_desired_size_set(width, height) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(66, 1, width, height);
+ GodotDisplayScreen.desired_size = [ width, height ];
+ GodotDisplayScreen.updateSize();
+}
+
+function _godot_js_display_fullscreen_cb(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(67, 1, callback);
+ const canvas = GodotConfig.canvas;
+ const func = GodotRuntime.get_func(callback);
+ function change_cb(evt) {
+ if (evt.target === canvas) {
+ func(GodotDisplayScreen.isFullscreen());
+ }
+ }
+ GodotEventListeners.add(document, "fullscreenchange", change_cb, false);
+ GodotEventListeners.add(document, "mozfullscreenchange", change_cb, false);
+ GodotEventListeners.add(document, "webkitfullscreenchange", change_cb, false);
+}
+
+function _godot_js_display_fullscreen_exit() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(68, 1);
+ return GodotDisplayScreen.exitFullscreen();
+}
+
+function _godot_js_display_fullscreen_request() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(69, 1);
+ return GodotDisplayScreen.requestFullscreen();
+}
+
+function _godot_js_display_has_webgl(p_version) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(70, 1, p_version);
+ if (p_version !== 1 && p_version !== 2) {
+ return false;
+ }
+ try {
+ return !!document.createElement("canvas").getContext(p_version === 2 ? "webgl2" : "webgl");
+ } catch (e) {}
+ return false;
+}
+
+function _godot_js_display_is_swap_ok_cancel() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(71, 1);
+ const win = [ "Windows", "Win64", "Win32", "WinCE" ];
+ const plat = navigator.platform || "";
+ if (win.indexOf(plat) !== -1) {
+ return 1;
+ }
+ return 0;
+}
+
+function _godot_js_display_notification_cb(callback, p_enter, p_exit, p_in, p_out) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(72, 1, callback, p_enter, p_exit, p_in, p_out);
+ const canvas = GodotConfig.canvas;
+ const func = GodotRuntime.get_func(callback);
+ const notif = [ p_enter, p_exit, p_in, p_out ];
+ [ "mouseover", "mouseleave", "focus", "blur" ].forEach(function(evt_name, idx) {
+ GodotEventListeners.add(canvas, evt_name, function() {
+ func(notif[idx]);
+ }, true);
+ });
+}
+
+function _godot_js_display_pixel_ratio_get() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(73, 1);
+ return GodotDisplayScreen.getPixelRatio();
+}
+
+function _godot_js_display_screen_dpi_get() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(74, 1);
+ return GodotDisplay.getDPI();
+}
+
+function _godot_js_display_screen_size_get(width, height) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(75, 1, width, height);
+ const scale = GodotDisplayScreen.getPixelRatio();
+ GodotRuntime.setHeapValue(width, window.screen.width * scale, "i32");
+ GodotRuntime.setHeapValue(height, window.screen.height * scale, "i32");
+}
+
+function _godot_js_display_setup_canvas(p_width, p_height, p_fullscreen, p_hidpi) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(76, 1, p_width, p_height, p_fullscreen, p_hidpi);
+ const canvas = GodotConfig.canvas;
+ GodotEventListeners.add(canvas, "contextmenu", function(ev) {
+ ev.preventDefault();
+ }, false);
+ GodotEventListeners.add(canvas, "webglcontextlost", function(ev) {
+ alert("WebGL context lost, please reload the page");
+ ev.preventDefault();
+ }, false);
+ GodotDisplayScreen.hidpi = !!p_hidpi;
+ switch (GodotConfig.canvas_resize_policy) {
+ case 0:
+ GodotDisplayScreen.desired_size = [ canvas.width, canvas.height ];
+ break;
+
+ case 1:
+ GodotDisplayScreen.desired_size = [ p_width, p_height ];
+ break;
+
+ default:
+ canvas.style.position = "absolute";
+ canvas.style.top = 0;
+ canvas.style.left = 0;
+ break;
+ }
+ GodotDisplayScreen.updateSize();
+ if (p_fullscreen) {
+ GodotDisplayScreen.requestFullscreen();
+ }
+}
+
+function _godot_js_display_size_update() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(77, 1);
+ const updated = GodotDisplayScreen.updateSize();
+ if (updated) {
+ GodotDisplayVK.updateSize();
+ }
+ return updated;
+}
+
+function _godot_js_display_touchscreen_is_available() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(78, 1);
+ return "ontouchstart" in window;
+}
+
+function _godot_js_display_tts_available() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(79, 1);
+ return "speechSynthesis" in window;
+}
+
+function _godot_js_display_vk_available() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(80, 1);
+ return GodotDisplayVK.available();
+}
+
+function _godot_js_display_vk_cb(p_input_cb) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(81, 1, p_input_cb);
+ const input_cb = GodotRuntime.get_func(p_input_cb);
+ if (GodotDisplayVK.available()) {
+ GodotDisplayVK.init(input_cb);
+ }
+}
+
+function _godot_js_display_vk_hide() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(82, 1);
+ GodotDisplayVK.hide();
+}
+
+function _godot_js_display_vk_show(p_text, p_type, p_start, p_end) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(83, 1, p_text, p_type, p_start, p_end);
+ const text = GodotRuntime.parseString(p_text);
+ const start = p_start > 0 ? p_start : 0;
+ const end = p_end > 0 ? p_end : start;
+ GodotDisplayVK.show(text, p_type, start, end);
+}
+
+function _godot_js_display_window_blur_cb(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(84, 1, callback);
+ const func = GodotRuntime.get_func(callback);
+ GodotEventListeners.add(window, "blur", function() {
+ func();
+ }, false);
+}
+
+function _godot_js_display_window_icon_set(p_ptr, p_len) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(85, 1, p_ptr, p_len);
+ let link = document.getElementById("-gd-engine-icon");
+ const old_icon = GodotDisplay.window_icon;
+ if (p_ptr) {
+ if (link === null) {
+ link = document.createElement("link");
+ link.rel = "icon";
+ link.id = "-gd-engine-icon";
+ document.head.appendChild(link);
+ }
+ const png = new Blob([ GodotRuntime.heapSlice(GROWABLE_HEAP_U8(), p_ptr, p_len) ], {
+ type: "image/png"
+ });
+ GodotDisplay.window_icon = URL.createObjectURL(png);
+ link.href = GodotDisplay.window_icon;
+ } else {
+ if (link) {
+ link.remove();
+ }
+ GodotDisplay.window_icon = null;
+ }
+ if (old_icon) {
+ URL.revokeObjectURL(old_icon);
+ }
+}
+
+function _godot_js_display_window_size_get(p_width, p_height) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(86, 1, p_width, p_height);
+ GodotRuntime.setHeapValue(p_width, GodotConfig.canvas.width, "i32");
+ GodotRuntime.setHeapValue(p_height, GodotConfig.canvas.height, "i32");
+}
+
+function _godot_js_display_window_title_set(p_data) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(87, 1, p_data);
+ document.title = GodotRuntime.parseString(p_data);
+}
+
+var IDHandler = {
+ _last_id: 0,
+ _references: {},
+ get: function(p_id) {
+ return IDHandler._references[p_id];
+ },
+ add: function(p_data) {
+ const id = ++IDHandler._last_id;
+ IDHandler._references[id] = p_data;
+ return id;
+ },
+ remove: function(p_id) {
+ delete IDHandler._references[p_id];
+ }
+};
+
+var GodotFetch = {
+ onread: function(id, result) {
+ const obj = IDHandler.get(id);
+ if (!obj) {
+ return;
+ }
+ if (result.value) {
+ obj.chunks.push(result.value);
+ }
+ obj.reading = false;
+ obj.done = result.done;
+ },
+ onresponse: function(id, response) {
+ const obj = IDHandler.get(id);
+ if (!obj) {
+ return;
+ }
+ let chunked = false;
+ response.headers.forEach(function(value, header) {
+ const v = value.toLowerCase().trim();
+ const h = header.toLowerCase().trim();
+ if (h === "transfer-encoding" && v === "chunked") {
+ chunked = true;
+ }
+ });
+ obj.status = response.status;
+ obj.response = response;
+ obj.reader = response.body.getReader();
+ obj.chunked = chunked;
+ },
+ onerror: function(id, err) {
+ GodotRuntime.error(err);
+ const obj = IDHandler.get(id);
+ if (!obj) {
+ return;
+ }
+ obj.error = err;
+ },
+ create: function(method, url, headers, body) {
+ const obj = {
+ request: null,
+ response: null,
+ reader: null,
+ error: null,
+ done: false,
+ reading: false,
+ status: 0,
+ chunks: []
+ };
+ const id = IDHandler.add(obj);
+ const init = {
+ method: method,
+ headers: headers,
+ body: body
+ };
+ obj.request = fetch(url, init);
+ obj.request.then(GodotFetch.onresponse.bind(null, id)).catch(GodotFetch.onerror.bind(null, id));
+ return id;
+ },
+ free: function(id) {
+ const obj = IDHandler.get(id);
+ if (!obj) {
+ return;
+ }
+ IDHandler.remove(id);
+ if (!obj.request) {
+ return;
+ }
+ obj.request.then(function(response) {
+ response.abort();
+ }).catch(function(e) {});
+ },
+ read: function(id) {
+ const obj = IDHandler.get(id);
+ if (!obj) {
+ return;
+ }
+ if (obj.reader && !obj.reading) {
+ if (obj.done) {
+ obj.reader = null;
+ return;
+ }
+ obj.reading = true;
+ obj.reader.read().then(GodotFetch.onread.bind(null, id)).catch(GodotFetch.onerror.bind(null, id));
+ }
+ }
+};
+
+function _godot_js_fetch_create(p_method, p_url, p_headers, p_headers_size, p_body, p_body_size) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(88, 1, p_method, p_url, p_headers, p_headers_size, p_body, p_body_size);
+ const method = GodotRuntime.parseString(p_method);
+ const url = GodotRuntime.parseString(p_url);
+ const headers = GodotRuntime.parseStringArray(p_headers, p_headers_size);
+ const body = p_body_size ? GodotRuntime.heapSlice(GROWABLE_HEAP_I8(), p_body, p_body_size) : null;
+ return GodotFetch.create(method, url, headers.map(function(hv) {
+ const idx = hv.indexOf(":");
+ if (idx <= 0) {
+ return [];
+ }
+ return [ hv.slice(0, idx).trim(), hv.slice(idx + 1).trim() ];
+ }).filter(function(v) {
+ return v.length === 2;
+ }), body);
+}
+
+function _godot_js_fetch_free(id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(89, 1, id);
+ GodotFetch.free(id);
+}
+
+function _godot_js_fetch_http_status_get(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(90, 1, p_id);
+ const obj = IDHandler.get(p_id);
+ if (!obj || !obj.response) {
+ return 0;
+ }
+ return obj.status;
+}
+
+function _godot_js_fetch_is_chunked(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(91, 1, p_id);
+ const obj = IDHandler.get(p_id);
+ if (!obj || !obj.response) {
+ return -1;
+ }
+ return obj.chunked ? 1 : 0;
+}
+
+function _godot_js_fetch_read_chunk(p_id, p_buf, p_buf_size) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(92, 1, p_id, p_buf, p_buf_size);
+ const obj = IDHandler.get(p_id);
+ if (!obj || !obj.response) {
+ return 0;
+ }
+ let to_read = p_buf_size;
+ const chunks = obj.chunks;
+ while (to_read && chunks.length) {
+ const chunk = obj.chunks[0];
+ if (chunk.length > to_read) {
+ GodotRuntime.heapCopy(GROWABLE_HEAP_I8(), chunk.slice(0, to_read), p_buf);
+ chunks[0] = chunk.slice(to_read);
+ to_read = 0;
+ } else {
+ GodotRuntime.heapCopy(GROWABLE_HEAP_I8(), chunk, p_buf);
+ to_read -= chunk.length;
+ chunks.pop();
+ }
+ }
+ if (!chunks.length) {
+ GodotFetch.read(p_id);
+ }
+ return p_buf_size - to_read;
+}
+
+function _godot_js_fetch_read_headers(p_id, p_parse_cb, p_ref) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(93, 1, p_id, p_parse_cb, p_ref);
+ const obj = IDHandler.get(p_id);
+ if (!obj || !obj.response) {
+ return 1;
+ }
+ const cb = GodotRuntime.get_func(p_parse_cb);
+ const arr = [];
+ obj.response.headers.forEach(function(v, h) {
+ arr.push(`${h}:${v}`);
+ });
+ const c_ptr = GodotRuntime.allocStringArray(arr);
+ cb(arr.length, c_ptr, p_ref);
+ GodotRuntime.freeStringArray(c_ptr, arr.length);
+ return 0;
+}
+
+function _godot_js_fetch_state_get(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(94, 1, p_id);
+ const obj = IDHandler.get(p_id);
+ if (!obj) {
+ return -1;
+ }
+ if (obj.error) {
+ return -1;
+ }
+ if (!obj.response) {
+ return 0;
+ }
+ if (obj.reader) {
+ return 1;
+ }
+ if (obj.done) {
+ return 2;
+ }
+ return -1;
+}
+
+var GodotInputGamepads = {
+ samples: [],
+ get_pads: function() {
+ try {
+ const pads = navigator.getGamepads();
+ if (pads) {
+ return pads;
+ }
+ return [];
+ } catch (e) {
+ return [];
+ }
+ },
+ get_samples: function() {
+ return GodotInputGamepads.samples;
+ },
+ get_sample: function(index) {
+ const samples = GodotInputGamepads.samples;
+ return index < samples.length ? samples[index] : null;
+ },
+ sample: function() {
+ const pads = GodotInputGamepads.get_pads();
+ const samples = [];
+ for (let i = 0; i < pads.length; i++) {
+ const pad = pads[i];
+ if (!pad) {
+ samples.push(null);
+ continue;
+ }
+ const s = {
+ standard: pad.mapping === "standard",
+ buttons: [],
+ axes: [],
+ connected: pad.connected
+ };
+ for (let b = 0; b < pad.buttons.length; b++) {
+ s.buttons.push(pad.buttons[b].value);
+ }
+ for (let a = 0; a < pad.axes.length; a++) {
+ s.axes.push(pad.axes[a]);
+ }
+ samples.push(s);
+ }
+ GodotInputGamepads.samples = samples;
+ },
+ init: function(onchange) {
+ GodotInputGamepads.samples = [];
+ function add(pad) {
+ const guid = GodotInputGamepads.get_guid(pad);
+ const c_id = GodotRuntime.allocString(pad.id);
+ const c_guid = GodotRuntime.allocString(guid);
+ onchange(pad.index, 1, c_id, c_guid);
+ GodotRuntime.free(c_id);
+ GodotRuntime.free(c_guid);
+ }
+ const pads = GodotInputGamepads.get_pads();
+ for (let i = 0; i < pads.length; i++) {
+ if (pads[i]) {
+ add(pads[i]);
+ }
+ }
+ GodotEventListeners.add(window, "gamepadconnected", function(evt) {
+ if (evt.gamepad) {
+ add(evt.gamepad);
+ }
+ }, false);
+ GodotEventListeners.add(window, "gamepaddisconnected", function(evt) {
+ if (evt.gamepad) {
+ onchange(evt.gamepad.index, 0);
+ }
+ }, false);
+ },
+ get_guid: function(pad) {
+ if (pad.mapping) {
+ return pad.mapping;
+ }
+ const ua = navigator.userAgent;
+ let os = "Unknown";
+ if (ua.indexOf("Android") >= 0) {
+ os = "Android";
+ } else if (ua.indexOf("Linux") >= 0) {
+ os = "Linux";
+ } else if (ua.indexOf("iPhone") >= 0) {
+ os = "iOS";
+ } else if (ua.indexOf("Macintosh") >= 0) {
+ os = "MacOSX";
+ } else if (ua.indexOf("Windows") >= 0) {
+ os = "Windows";
+ }
+ const id = pad.id;
+ const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i;
+ const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i;
+ let vendor = "";
+ let product = "";
+ if (exp1.test(id)) {
+ const match = exp1.exec(id);
+ vendor = match[1].padStart(4, "0");
+ product = match[2].padStart(4, "0");
+ } else if (exp2.test(id)) {
+ const match = exp2.exec(id);
+ vendor = match[1].padStart(4, "0");
+ product = match[2].padStart(4, "0");
+ }
+ if (!vendor || !product) {
+ return `${os}Unknown`;
+ }
+ return os + vendor + product;
+ }
+};
+
+var GodotInputDragDrop = {
+ promises: [],
+ pending_files: [],
+ add_entry: function(entry) {
+ if (entry.isDirectory) {
+ GodotInputDragDrop.add_dir(entry);
+ } else if (entry.isFile) {
+ GodotInputDragDrop.add_file(entry);
+ } else {
+ GodotRuntime.error("Unrecognized entry...", entry);
+ }
+ },
+ add_dir: function(entry) {
+ GodotInputDragDrop.promises.push(new Promise(function(resolve, reject) {
+ const reader = entry.createReader();
+ reader.readEntries(function(entries) {
+ for (let i = 0; i < entries.length; i++) {
+ GodotInputDragDrop.add_entry(entries[i]);
+ }
+ resolve();
+ });
+ }));
+ },
+ add_file: function(entry) {
+ GodotInputDragDrop.promises.push(new Promise(function(resolve, reject) {
+ entry.file(function(file) {
+ const reader = new FileReader();
+ reader.onload = function() {
+ const f = {
+ "path": file.relativePath || file.webkitRelativePath,
+ "name": file.name,
+ "type": file.type,
+ "size": file.size,
+ "data": reader.result
+ };
+ if (!f["path"]) {
+ f["path"] = f["name"];
+ }
+ GodotInputDragDrop.pending_files.push(f);
+ resolve();
+ };
+ reader.onerror = function() {
+ GodotRuntime.print("Error reading file");
+ reject();
+ };
+ reader.readAsArrayBuffer(file);
+ }, function(err) {
+ GodotRuntime.print("Error!");
+ reject();
+ });
+ }));
+ },
+ process: function(resolve, reject) {
+ if (GodotInputDragDrop.promises.length === 0) {
+ resolve();
+ return;
+ }
+ GodotInputDragDrop.promises.pop().then(function() {
+ setTimeout(function() {
+ GodotInputDragDrop.process(resolve, reject);
+ }, 0);
+ });
+ },
+ _process_event: function(ev, callback) {
+ ev.preventDefault();
+ if (ev.dataTransfer.items) {
+ for (let i = 0; i < ev.dataTransfer.items.length; i++) {
+ const item = ev.dataTransfer.items[i];
+ let entry = null;
+ if ("getAsEntry" in item) {
+ entry = item.getAsEntry();
+ } else if ("webkitGetAsEntry" in item) {
+ entry = item.webkitGetAsEntry();
+ }
+ if (entry) {
+ GodotInputDragDrop.add_entry(entry);
+ }
+ }
+ } else {
+ GodotRuntime.error("File upload not supported");
+ }
+ new Promise(GodotInputDragDrop.process).then(function() {
+ const DROP = `/tmp/drop-${parseInt(Math.random() * (1 << 30), 10)}/`;
+ const drops = [];
+ const files = [];
+ FS.mkdir(DROP.slice(0, -1));
+ GodotInputDragDrop.pending_files.forEach(elem => {
+ const path = elem["path"];
+ GodotFS.copy_to_fs(DROP + path, elem["data"]);
+ let idx = path.indexOf("/");
+ if (idx === -1) {
+ drops.push(DROP + path);
+ } else {
+ const sub = path.substr(0, idx);
+ idx = sub.indexOf("/");
+ if (idx < 0 && drops.indexOf(DROP + sub) === -1) {
+ drops.push(DROP + sub);
+ }
+ }
+ files.push(DROP + path);
+ });
+ GodotInputDragDrop.promises = [];
+ GodotInputDragDrop.pending_files = [];
+ callback(drops);
+ if (GodotConfig.persistent_drops) {
+ GodotOS.atexit(function(resolve, reject) {
+ GodotInputDragDrop.remove_drop(files, DROP);
+ resolve();
+ });
+ } else {
+ GodotInputDragDrop.remove_drop(files, DROP);
+ }
+ });
+ },
+ remove_drop: function(files, drop_path) {
+ const dirs = [ drop_path.substr(0, drop_path.length - 1) ];
+ files.forEach(function(file) {
+ FS.unlink(file);
+ let dir = file.replace(drop_path, "");
+ let idx = dir.lastIndexOf("/");
+ while (idx > 0) {
+ dir = dir.substr(0, idx);
+ if (dirs.indexOf(drop_path + dir) === -1) {
+ dirs.push(drop_path + dir);
+ }
+ idx = dir.lastIndexOf("/");
+ }
+ });
+ dirs.sort(function(a, b) {
+ const al = (a.match(/\//g) || []).length;
+ const bl = (b.match(/\//g) || []).length;
+ if (al > bl) {
+ return -1;
+ } else if (al < bl) {
+ return 1;
+ }
+ return 0;
+ }).forEach(function(dir) {
+ FS.rmdir(dir);
+ });
+ },
+ handler: function(callback) {
+ return function(ev) {
+ GodotInputDragDrop._process_event(ev, callback);
+ };
+ }
+};
+
+var GodotInput = {
+ getModifiers: function(evt) {
+ return evt.shiftKey + 0 + (evt.altKey + 0 << 1) + (evt.ctrlKey + 0 << 2) + (evt.metaKey + 0 << 3);
+ },
+ computePosition: function(evt, rect) {
+ const canvas = GodotConfig.canvas;
+ const rw = canvas.width / rect.width;
+ const rh = canvas.height / rect.height;
+ const x = (evt.clientX - rect.x) * rw;
+ const y = (evt.clientY - rect.y) * rh;
+ return [ x, y ];
+ }
+};
+
+function _godot_js_input_drop_files_cb(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(95, 1, callback);
+ const func = GodotRuntime.get_func(callback);
+ const dropFiles = function(files) {
+ const args = files || [];
+ if (!args.length) {
+ return;
+ }
+ const argc = args.length;
+ const argv = GodotRuntime.allocStringArray(args);
+ func(argv, argc);
+ GodotRuntime.freeStringArray(argv, argc);
+ };
+ const canvas = GodotConfig.canvas;
+ GodotEventListeners.add(canvas, "dragover", function(ev) {
+ ev.preventDefault();
+ }, false);
+ GodotEventListeners.add(canvas, "drop", GodotInputDragDrop.handler(dropFiles));
+}
+
+function _godot_js_input_gamepad_cb(change_cb) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(96, 1, change_cb);
+ const onchange = GodotRuntime.get_func(change_cb);
+ GodotInputGamepads.init(onchange);
+}
+
+function _godot_js_input_gamepad_sample() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(97, 1);
+ GodotInputGamepads.sample();
+ return 0;
+}
+
+function _godot_js_input_gamepad_sample_count() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(98, 1);
+ return GodotInputGamepads.get_samples().length;
+}
+
+function _godot_js_input_gamepad_sample_get(p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(99, 1, p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard);
+ const sample = GodotInputGamepads.get_sample(p_index);
+ if (!sample || !sample.connected) {
+ return 1;
+ }
+ const btns = sample.buttons;
+ const btns_len = btns.length < 16 ? btns.length : 16;
+ for (let i = 0; i < btns_len; i++) {
+ GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], "float");
+ }
+ GodotRuntime.setHeapValue(r_btns_num, btns_len, "i32");
+ const axes = sample.axes;
+ const axes_len = axes.length < 10 ? axes.length : 10;
+ for (let i = 0; i < axes_len; i++) {
+ GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], "float");
+ }
+ GodotRuntime.setHeapValue(r_axes_num, axes_len, "i32");
+ const is_standard = sample.standard ? 1 : 0;
+ GodotRuntime.setHeapValue(r_standard, is_standard, "i32");
+ return 0;
+}
+
+function _godot_js_input_key_cb(callback, code, key) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(100, 1, callback, code, key);
+ const func = GodotRuntime.get_func(callback);
+ function key_cb(pressed, evt) {
+ const modifiers = GodotInput.getModifiers(evt);
+ GodotRuntime.stringToHeap(evt.code, code, 32);
+ GodotRuntime.stringToHeap(evt.key, key, 32);
+ func(pressed, evt.repeat, modifiers);
+ evt.preventDefault();
+ }
+ GodotEventListeners.add(GodotConfig.canvas, "keydown", key_cb.bind(null, 1), false);
+ GodotEventListeners.add(GodotConfig.canvas, "keyup", key_cb.bind(null, 0), false);
+}
+
+function _godot_js_input_mouse_button_cb(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(101, 1, callback);
+ const func = GodotRuntime.get_func(callback);
+ const canvas = GodotConfig.canvas;
+ function button_cb(p_pressed, evt) {
+ const rect = canvas.getBoundingClientRect();
+ const pos = GodotInput.computePosition(evt, rect);
+ const modifiers = GodotInput.getModifiers(evt);
+ if (p_pressed) {
+ GodotConfig.canvas.focus();
+ }
+ if (func(p_pressed, evt.button, pos[0], pos[1], modifiers)) {
+ evt.preventDefault();
+ }
+ }
+ GodotEventListeners.add(canvas, "mousedown", button_cb.bind(null, 1), false);
+ GodotEventListeners.add(window, "mouseup", button_cb.bind(null, 0), false);
+}
+
+function _godot_js_input_mouse_move_cb(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(102, 1, callback);
+ const func = GodotRuntime.get_func(callback);
+ const canvas = GodotConfig.canvas;
+ function move_cb(evt) {
+ const rect = canvas.getBoundingClientRect();
+ const pos = GodotInput.computePosition(evt, rect);
+ const rw = canvas.width / rect.width;
+ const rh = canvas.height / rect.height;
+ const rel_pos_x = evt.movementX * rw;
+ const rel_pos_y = evt.movementY * rh;
+ const modifiers = GodotInput.getModifiers(evt);
+ func(pos[0], pos[1], rel_pos_x, rel_pos_y, modifiers);
+ }
+ GodotEventListeners.add(window, "mousemove", move_cb, false);
+}
+
+function _godot_js_input_mouse_wheel_cb(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(103, 1, callback);
+ const func = GodotRuntime.get_func(callback);
+ function wheel_cb(evt) {
+ if (func(evt["deltaX"] || 0, evt["deltaY"] || 0)) {
+ evt.preventDefault();
+ }
+ }
+ GodotEventListeners.add(GodotConfig.canvas, "wheel", wheel_cb, false);
+}
+
+function _godot_js_input_paste_cb(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(104, 1, callback);
+ const func = GodotRuntime.get_func(callback);
+ GodotEventListeners.add(window, "paste", function(evt) {
+ const text = evt.clipboardData.getData("text");
+ const ptr = GodotRuntime.allocString(text);
+ func(ptr);
+ GodotRuntime.free(ptr);
+ }, false);
+}
+
+function _godot_js_input_touch_cb(callback, ids, coords) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(105, 1, callback, ids, coords);
+ const func = GodotRuntime.get_func(callback);
+ const canvas = GodotConfig.canvas;
+ function touch_cb(type, evt) {
+ if (type === 0) {
+ GodotConfig.canvas.focus();
+ }
+ const rect = canvas.getBoundingClientRect();
+ const touches = evt.changedTouches;
+ for (let i = 0; i < touches.length; i++) {
+ const touch = touches[i];
+ const pos = GodotInput.computePosition(touch, rect);
+ GodotRuntime.setHeapValue(coords + i * 2 * 8, pos[0], "double");
+ GodotRuntime.setHeapValue(coords + (i * 2 + 1) * 8, pos[1], "double");
+ GodotRuntime.setHeapValue(ids + i * 4, touch.identifier, "i32");
+ }
+ func(type, touches.length);
+ if (evt.cancelable) {
+ evt.preventDefault();
+ }
+ }
+ GodotEventListeners.add(canvas, "touchstart", touch_cb.bind(null, 0), false);
+ GodotEventListeners.add(canvas, "touchend", touch_cb.bind(null, 1), false);
+ GodotEventListeners.add(canvas, "touchcancel", touch_cb.bind(null, 1), false);
+ GodotEventListeners.add(canvas, "touchmove", touch_cb.bind(null, 2), false);
+}
+
+function _godot_js_input_vibrate_handheld(p_duration_ms) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(106, 1, p_duration_ms);
+ if (typeof navigator.vibrate !== "function") {
+ GodotRuntime.print("This browser does not support vibration.");
+ } else {
+ navigator.vibrate(p_duration_ms);
+ }
+}
+
+function _godot_js_os_download_buffer(p_ptr, p_size, p_name, p_mime) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(107, 1, p_ptr, p_size, p_name, p_mime);
+ const buf = GodotRuntime.heapSlice(GROWABLE_HEAP_I8(), p_ptr, p_size);
+ const name = GodotRuntime.parseString(p_name);
+ const mime = GodotRuntime.parseString(p_mime);
+ const blob = new Blob([ buf ], {
+ type: mime
+ });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = name;
+ a.style.display = "none";
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ window.URL.revokeObjectURL(url);
+}
+
+function _godot_js_os_execute(p_json) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(108, 1, p_json);
+ const json_args = GodotRuntime.parseString(p_json);
+ const args = JSON.parse(json_args);
+ if (GodotConfig.on_execute) {
+ GodotConfig.on_execute(args);
+ return 0;
+ }
+ return 1;
+}
+
+function _godot_js_os_finish_async(p_callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(109, 1, p_callback);
+ const func = GodotRuntime.get_func(p_callback);
+ GodotOS.finish_async(func);
+}
+
+function _godot_js_os_fs_is_persistent() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(110, 1);
+ return GodotFS.is_persistent();
+}
+
+function _godot_js_os_fs_sync(callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(111, 1, callback);
+ const func = GodotRuntime.get_func(callback);
+ GodotOS._fs_sync_promise = GodotFS.sync();
+ GodotOS._fs_sync_promise.then(function(err) {
+ func();
+ });
+}
+
+function _godot_js_os_has_feature(p_ftr) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(112, 1, p_ftr);
+ const ftr = GodotRuntime.parseString(p_ftr);
+ const ua = navigator.userAgent;
+ if (ftr === "web_macos") {
+ return ua.indexOf("Mac") !== -1 ? 1 : 0;
+ }
+ if (ftr === "web_windows") {
+ return ua.indexOf("Windows") !== -1 ? 1 : 0;
+ }
+ if (ftr === "web_android") {
+ return ua.indexOf("Android") !== -1 ? 1 : 0;
+ }
+ if (ftr === "web_ios") {
+ return ua.indexOf("iPhone") !== -1 || ua.indexOf("iPad") !== -1 || ua.indexOf("iPod") !== -1 ? 1 : 0;
+ }
+ if (ftr === "web_linuxbsd") {
+ return ua.indexOf("CrOS") !== -1 || ua.indexOf("BSD") !== -1 || ua.indexOf("Linux") !== -1 || ua.indexOf("X11") !== -1 ? 1 : 0;
+ }
+ return 0;
+}
+
+function _godot_js_os_hw_concurrency_get() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(113, 1);
+ const concurrency = navigator.hardwareConcurrency || 1;
+ return concurrency < 2 ? concurrency : 2;
+}
+
+function _godot_js_os_request_quit_cb(p_callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(114, 1, p_callback);
+ GodotOS.request_quit = GodotRuntime.get_func(p_callback);
+}
+
+function _godot_js_os_shell_open(p_uri) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(115, 1, p_uri);
+ window.open(GodotRuntime.parseString(p_uri), "_blank");
+}
+
+var GodotPWA = {
+ hasUpdate: false,
+ updateState: function(cb, reg) {
+ if (!reg) {
+ return;
+ }
+ if (!reg.active) {
+ return;
+ }
+ if (reg.waiting) {
+ GodotPWA.hasUpdate = true;
+ cb();
+ }
+ GodotEventListeners.add(reg, "updatefound", function() {
+ const installing = reg.installing;
+ GodotEventListeners.add(installing, "statechange", function() {
+ if (installing.state === "installed") {
+ GodotPWA.hasUpdate = true;
+ cb();
+ }
+ });
+ });
+ }
+};
+
+function _godot_js_pwa_cb(p_update_cb) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(116, 1, p_update_cb);
+ if ("serviceWorker" in navigator) {
+ const cb = GodotRuntime.get_func(p_update_cb);
+ navigator.serviceWorker.getRegistration().then(GodotPWA.updateState.bind(null, cb));
+ }
+}
+
+function _godot_js_pwa_update() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(117, 1);
+ if ("serviceWorker" in navigator && GodotPWA.hasUpdate) {
+ navigator.serviceWorker.getRegistration().then(function(reg) {
+ if (!reg || !reg.waiting) {
+ return;
+ }
+ reg.waiting.postMessage("update");
+ });
+ return 0;
+ }
+ return 1;
+}
+
+var GodotRTCDataChannel = {
+ connect: function(p_id, p_on_open, p_on_message, p_on_error, p_on_close) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ ref.binaryType = "arraybuffer";
+ ref.onopen = function(event) {
+ p_on_open();
+ };
+ ref.onclose = function(event) {
+ p_on_close();
+ };
+ ref.onerror = function(event) {
+ p_on_error();
+ };
+ ref.onmessage = function(event) {
+ let buffer;
+ let is_string = 0;
+ if (event.data instanceof ArrayBuffer) {
+ buffer = new Uint8Array(event.data);
+ } else if (event.data instanceof Blob) {
+ GodotRuntime.error("Blob type not supported");
+ return;
+ } else if (typeof event.data === "string") {
+ is_string = 1;
+ const enc = new TextEncoder("utf-8");
+ buffer = new Uint8Array(enc.encode(event.data));
+ } else {
+ GodotRuntime.error("Unknown message type");
+ return;
+ }
+ const len = buffer.length * buffer.BYTES_PER_ELEMENT;
+ const out = GodotRuntime.malloc(len);
+ GROWABLE_HEAP_U8().set(buffer, out);
+ p_on_message(out, len, is_string);
+ GodotRuntime.free(out);
+ };
+ },
+ close: function(p_id) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ ref.onopen = null;
+ ref.onmessage = null;
+ ref.onerror = null;
+ ref.onclose = null;
+ ref.close();
+ },
+ get_prop: function(p_id, p_prop, p_def) {
+ const ref = IDHandler.get(p_id);
+ return ref && ref[p_prop] !== undefined ? ref[p_prop] : p_def;
+ }
+};
+
+function _godot_js_rtc_datachannel_close(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(118, 1, p_id);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ GodotRTCDataChannel.close(p_id);
+}
+
+function _godot_js_rtc_datachannel_connect(p_id, p_ref, p_on_open, p_on_message, p_on_error, p_on_close) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(119, 1, p_id, p_ref, p_on_open, p_on_message, p_on_error, p_on_close);
+ const onopen = GodotRuntime.get_func(p_on_open).bind(null, p_ref);
+ const onmessage = GodotRuntime.get_func(p_on_message).bind(null, p_ref);
+ const onerror = GodotRuntime.get_func(p_on_error).bind(null, p_ref);
+ const onclose = GodotRuntime.get_func(p_on_close).bind(null, p_ref);
+ GodotRTCDataChannel.connect(p_id, onopen, onmessage, onerror, onclose);
+}
+
+function _godot_js_rtc_datachannel_destroy(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(120, 1, p_id);
+ GodotRTCDataChannel.close(p_id);
+ IDHandler.remove(p_id);
+}
+
+function _godot_js_rtc_datachannel_get_buffered_amount(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(121, 1, p_id);
+ return GodotRTCDataChannel.get_prop(p_id, "bufferedAmount", 0);
+}
+
+function _godot_js_rtc_datachannel_id_get(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(122, 1, p_id);
+ return GodotRTCDataChannel.get_prop(p_id, "id", 65535);
+}
+
+function _godot_js_rtc_datachannel_is_negotiated(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(123, 1, p_id);
+ return GodotRTCDataChannel.get_prop(p_id, "negotiated", 65535);
+}
+
+function _godot_js_rtc_datachannel_is_ordered(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(124, 1, p_id);
+ return GodotRTCDataChannel.get_prop(p_id, "ordered", true);
+}
+
+function _godot_js_rtc_datachannel_label_get(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(125, 1, p_id);
+ const ref = IDHandler.get(p_id);
+ if (!ref || !ref.label) {
+ return 0;
+ }
+ return GodotRuntime.allocString(ref.label);
+}
+
+function _godot_js_rtc_datachannel_max_packet_lifetime_get(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(126, 1, p_id);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return 65535;
+ }
+ if (ref["maxPacketLifeTime"] !== undefined) {
+ return ref["maxPacketLifeTime"];
+ } else if (ref["maxRetransmitTime"] !== undefined) {
+ return ref["maxRetransmitTime"];
+ }
+ return 65535;
+}
+
+function _godot_js_rtc_datachannel_max_retransmits_get(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(127, 1, p_id);
+ return GodotRTCDataChannel.get_prop(p_id, "maxRetransmits", 65535);
+}
+
+function _godot_js_rtc_datachannel_protocol_get(p_id) {
+ const ref = IDHandler.get(p_id);
+ if (!ref || !ref.protocol) {
+ return 0;
+ }
+ return GodotRuntime.allocString(ref.protocol);
+}
+
+function _godot_js_rtc_datachannel_ready_state_get(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(128, 1, p_id);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return 3;
+ }
+ switch (ref.readyState) {
+ case "connecting":
+ return 0;
+
+ case "open":
+ return 1;
+
+ case "closing":
+ return 2;
+
+ case "closed":
+ default:
+ return 3;
+ }
+}
+
+function _godot_js_rtc_datachannel_send(p_id, p_buffer, p_length, p_raw) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(129, 1, p_id, p_buffer, p_length, p_raw);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return 1;
+ }
+ const bytes_array = new Uint8Array(p_length);
+ for (let i = 0; i < p_length; i++) {
+ bytes_array[i] = GodotRuntime.getHeapValue(p_buffer + i, "i8");
+ }
+ if (p_raw) {
+ ref.send(bytes_array.buffer);
+ } else {
+ const string = new TextDecoder("utf-8").decode(bytes_array);
+ ref.send(string);
+ }
+ return 0;
+}
+
+var GodotRTCPeerConnection = {
+ ConnectionState: {
+ new: 0,
+ connecting: 1,
+ connected: 2,
+ disconnected: 3,
+ failed: 4,
+ closed: 5
+ },
+ ConnectionStateCompat: {
+ new: 0,
+ checking: 1,
+ connected: 2,
+ completed: 2,
+ disconnected: 3,
+ failed: 4,
+ closed: 5
+ },
+ IceGatheringState: {
+ new: 0,
+ gathering: 1,
+ complete: 2
+ },
+ SignalingState: {
+ stable: 0,
+ "have-local-offer": 1,
+ "have-remote-offer": 2,
+ "have-local-pranswer": 3,
+ "have-remote-pranswer": 4,
+ closed: 5
+ },
+ create: function(config, onConnectionChange, onSignalingChange, onIceGatheringChange, onIceCandidate, onDataChannel) {
+ let conn = null;
+ try {
+ conn = new RTCPeerConnection(config);
+ } catch (e) {
+ GodotRuntime.error(e);
+ return 0;
+ }
+ const id = IDHandler.add(conn);
+ if ("connectionState" in conn && conn["connectionState"] !== undefined) {
+ conn.onconnectionstatechange = function(event) {
+ if (!IDHandler.get(id)) {
+ return;
+ }
+ onConnectionChange(GodotRTCPeerConnection.ConnectionState[conn.connectionState] || 0);
+ };
+ } else {
+ conn.oniceconnectionstatechange = function(event) {
+ if (!IDHandler.get(id)) {
+ return;
+ }
+ onConnectionChange(GodotRTCPeerConnection.ConnectionStateCompat[conn.iceConnectionState] || 0);
+ };
+ }
+ conn.onicegatheringstatechange = function(event) {
+ if (!IDHandler.get(id)) {
+ return;
+ }
+ onIceGatheringChange(GodotRTCPeerConnection.IceGatheringState[conn.iceGatheringState] || 0);
+ };
+ conn.onsignalingstatechange = function(event) {
+ if (!IDHandler.get(id)) {
+ return;
+ }
+ onSignalingChange(GodotRTCPeerConnection.SignalingState[conn.signalingState] || 0);
+ };
+ conn.onicecandidate = function(event) {
+ if (!IDHandler.get(id)) {
+ return;
+ }
+ const c = event.candidate;
+ if (!c || !c.candidate) {
+ return;
+ }
+ const candidate_str = GodotRuntime.allocString(c.candidate);
+ const mid_str = GodotRuntime.allocString(c.sdpMid);
+ onIceCandidate(mid_str, c.sdpMLineIndex, candidate_str);
+ GodotRuntime.free(candidate_str);
+ GodotRuntime.free(mid_str);
+ };
+ conn.ondatachannel = function(event) {
+ if (!IDHandler.get(id)) {
+ return;
+ }
+ const cid = IDHandler.add(event.channel);
+ onDataChannel(cid);
+ };
+ return id;
+ },
+ destroy: function(p_id) {
+ const conn = IDHandler.get(p_id);
+ if (!conn) {
+ return;
+ }
+ conn.onconnectionstatechange = null;
+ conn.oniceconnectionstatechange = null;
+ conn.onicegatheringstatechange = null;
+ conn.onsignalingstatechange = null;
+ conn.onicecandidate = null;
+ conn.ondatachannel = null;
+ IDHandler.remove(p_id);
+ },
+ onsession: function(p_id, callback, session) {
+ if (!IDHandler.get(p_id)) {
+ return;
+ }
+ const type_str = GodotRuntime.allocString(session.type);
+ const sdp_str = GodotRuntime.allocString(session.sdp);
+ callback(type_str, sdp_str);
+ GodotRuntime.free(type_str);
+ GodotRuntime.free(sdp_str);
+ },
+ onerror: function(p_id, callback, error) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ GodotRuntime.error(error);
+ callback();
+ }
+};
+
+function _godot_js_rtc_pc_close(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(130, 1, p_id);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ ref.close();
+}
+
+function _godot_js_rtc_pc_create(p_config, p_ref, p_on_connection_state_change, p_on_ice_gathering_state_change, p_on_signaling_state_change, p_on_ice_candidate, p_on_datachannel) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(131, 1, p_config, p_ref, p_on_connection_state_change, p_on_ice_gathering_state_change, p_on_signaling_state_change, p_on_ice_candidate, p_on_datachannel);
+ const wrap = function(p_func) {
+ return GodotRuntime.get_func(p_func).bind(null, p_ref);
+ };
+ return GodotRTCPeerConnection.create(JSON.parse(GodotRuntime.parseString(p_config)), wrap(p_on_connection_state_change), wrap(p_on_signaling_state_change), wrap(p_on_ice_gathering_state_change), wrap(p_on_ice_candidate), wrap(p_on_datachannel));
+}
+
+function _godot_js_rtc_pc_datachannel_create(p_id, p_label, p_config) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(132, 1, p_id, p_label, p_config);
+ try {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return 0;
+ }
+ const label = GodotRuntime.parseString(p_label);
+ const config = JSON.parse(GodotRuntime.parseString(p_config));
+ const channel = ref.createDataChannel(label, config);
+ return IDHandler.add(channel);
+ } catch (e) {
+ GodotRuntime.error(e);
+ return 0;
+ }
+}
+
+function _godot_js_rtc_pc_destroy(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(133, 1, p_id);
+ GodotRTCPeerConnection.destroy(p_id);
+}
+
+function _godot_js_rtc_pc_ice_candidate_add(p_id, p_mid_name, p_mline_idx, p_sdp) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(134, 1, p_id, p_mid_name, p_mline_idx, p_sdp);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ const sdpMidName = GodotRuntime.parseString(p_mid_name);
+ const sdpName = GodotRuntime.parseString(p_sdp);
+ ref.addIceCandidate(new RTCIceCandidate({
+ "candidate": sdpName,
+ "sdpMid": sdpMidName,
+ "sdpMlineIndex": p_mline_idx
+ }));
+}
+
+function _godot_js_rtc_pc_local_description_set(p_id, p_type, p_sdp, p_obj, p_on_error) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(135, 1, p_id, p_type, p_sdp, p_obj, p_on_error);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ const type = GodotRuntime.parseString(p_type);
+ const sdp = GodotRuntime.parseString(p_sdp);
+ const onerror = GodotRuntime.get_func(p_on_error).bind(null, p_obj);
+ ref.setLocalDescription({
+ "sdp": sdp,
+ "type": type
+ }).catch(function(error) {
+ GodotRTCPeerConnection.onerror(p_id, onerror, error);
+ });
+}
+
+function _godot_js_rtc_pc_offer_create(p_id, p_obj, p_on_session, p_on_error) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(136, 1, p_id, p_obj, p_on_session, p_on_error);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ const onsession = GodotRuntime.get_func(p_on_session).bind(null, p_obj);
+ const onerror = GodotRuntime.get_func(p_on_error).bind(null, p_obj);
+ ref.createOffer().then(function(session) {
+ GodotRTCPeerConnection.onsession(p_id, onsession, session);
+ }).catch(function(error) {
+ GodotRTCPeerConnection.onerror(p_id, onerror, error);
+ });
+}
+
+function _godot_js_rtc_pc_remote_description_set(p_id, p_type, p_sdp, p_obj, p_session_created, p_on_error) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(137, 1, p_id, p_type, p_sdp, p_obj, p_session_created, p_on_error);
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ const type = GodotRuntime.parseString(p_type);
+ const sdp = GodotRuntime.parseString(p_sdp);
+ const onerror = GodotRuntime.get_func(p_on_error).bind(null, p_obj);
+ const onsession = GodotRuntime.get_func(p_session_created).bind(null, p_obj);
+ ref.setRemoteDescription({
+ "sdp": sdp,
+ "type": type
+ }).then(function() {
+ if (type !== "offer") {
+ return Promise.resolve();
+ }
+ return ref.createAnswer().then(function(session) {
+ GodotRTCPeerConnection.onsession(p_id, onsession, session);
+ });
+ }).catch(function(error) {
+ GodotRTCPeerConnection.onerror(p_id, onerror, error);
+ });
+}
+
+function _godot_js_tts_get_voices(p_callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(138, 1, p_callback);
+ const func = GodotRuntime.get_func(p_callback);
+ try {
+ const arr = [];
+ const voices = window.speechSynthesis.getVoices();
+ for (let i = 0; i < voices.length; i++) {
+ arr.push(`${voices[i].lang};${voices[i].name}`);
+ }
+ const c_ptr = GodotRuntime.allocStringArray(arr);
+ func(arr.length, c_ptr);
+ GodotRuntime.freeStringArray(c_ptr, arr.length);
+ } catch (e) {}
+}
+
+function _godot_js_tts_is_paused() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(139, 1);
+ return window.speechSynthesis.paused;
+}
+
+function _godot_js_tts_is_speaking() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(140, 1);
+ return window.speechSynthesis.speaking;
+}
+
+function _godot_js_tts_pause() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(141, 1);
+ window.speechSynthesis.pause();
+}
+
+function _godot_js_tts_resume() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(142, 1);
+ window.speechSynthesis.resume();
+}
+
+function _godot_js_tts_speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_callback) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(143, 1, p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_callback);
+ const func = GodotRuntime.get_func(p_callback);
+ function listener_end(evt) {
+ evt.currentTarget.cb(1, evt.currentTarget.id, 0);
+ }
+ function listener_start(evt) {
+ evt.currentTarget.cb(0, evt.currentTarget.id, 0);
+ }
+ function listener_error(evt) {
+ evt.currentTarget.cb(2, evt.currentTarget.id, 0);
+ }
+ function listener_bound(evt) {
+ evt.currentTarget.cb(3, evt.currentTarget.id, evt.charIndex);
+ }
+ const utterance = new SpeechSynthesisUtterance(GodotRuntime.parseString(p_text));
+ utterance.rate = p_rate;
+ utterance.pitch = p_pitch;
+ utterance.volume = p_volume / 100;
+ utterance.addEventListener("end", listener_end);
+ utterance.addEventListener("start", listener_start);
+ utterance.addEventListener("error", listener_error);
+ utterance.addEventListener("boundary", listener_bound);
+ utterance.id = p_utterance_id;
+ utterance.cb = func;
+ const voice = GodotRuntime.parseString(p_voice);
+ const voices = window.speechSynthesis.getVoices();
+ for (let i = 0; i < voices.length; i++) {
+ if (voices[i].name === voice) {
+ utterance.voice = voices[i];
+ break;
+ }
+ }
+ window.speechSynthesis.resume();
+ window.speechSynthesis.speak(utterance);
+}
+
+function _godot_js_tts_stop() {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(144, 1);
+ window.speechSynthesis.cancel();
+ window.speechSynthesis.resume();
+}
+
+var GodotWebSocket = {
+ _onopen: function(p_id, callback, event) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ const c_str = GodotRuntime.allocString(ref.protocol);
+ callback(c_str);
+ GodotRuntime.free(c_str);
+ },
+ _onmessage: function(p_id, callback, event) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ let buffer;
+ let is_string = 0;
+ if (event.data instanceof ArrayBuffer) {
+ buffer = new Uint8Array(event.data);
+ } else if (event.data instanceof Blob) {
+ GodotRuntime.error("Blob type not supported");
+ return;
+ } else if (typeof event.data === "string") {
+ is_string = 1;
+ const enc = new TextEncoder("utf-8");
+ buffer = new Uint8Array(enc.encode(event.data));
+ } else {
+ GodotRuntime.error("Unknown message type");
+ return;
+ }
+ const len = buffer.length * buffer.BYTES_PER_ELEMENT;
+ const out = GodotRuntime.malloc(len);
+ GROWABLE_HEAP_U8().set(buffer, out);
+ callback(out, len, is_string);
+ GodotRuntime.free(out);
+ },
+ _onerror: function(p_id, callback, event) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ callback();
+ },
+ _onclose: function(p_id, callback, event) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ const c_str = GodotRuntime.allocString(event.reason);
+ callback(event.code, c_str, event.wasClean ? 1 : 0);
+ GodotRuntime.free(c_str);
+ },
+ send: function(p_id, p_data) {
+ const ref = IDHandler.get(p_id);
+ if (!ref || ref.readyState !== ref.OPEN) {
+ return 1;
+ }
+ ref.send(p_data);
+ return 0;
+ },
+ bufferedAmount: function(p_id) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return 0;
+ }
+ return ref.bufferedAmount;
+ },
+ create: function(socket, p_on_open, p_on_message, p_on_error, p_on_close) {
+ const id = IDHandler.add(socket);
+ socket.onopen = GodotWebSocket._onopen.bind(null, id, p_on_open);
+ socket.onmessage = GodotWebSocket._onmessage.bind(null, id, p_on_message);
+ socket.onerror = GodotWebSocket._onerror.bind(null, id, p_on_error);
+ socket.onclose = GodotWebSocket._onclose.bind(null, id, p_on_close);
+ return id;
+ },
+ close: function(p_id, p_code, p_reason) {
+ const ref = IDHandler.get(p_id);
+ if (ref && ref.readyState < ref.CLOSING) {
+ const code = p_code;
+ const reason = p_reason;
+ ref.close(code, reason);
+ }
+ },
+ destroy: function(p_id) {
+ const ref = IDHandler.get(p_id);
+ if (!ref) {
+ return;
+ }
+ GodotWebSocket.close(p_id, 3001, "destroyed");
+ IDHandler.remove(p_id);
+ ref.onopen = null;
+ ref.onmessage = null;
+ ref.onerror = null;
+ ref.onclose = null;
+ }
+};
+
+function _godot_js_websocket_buffered_amount(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(145, 1, p_id);
+ return GodotWebSocket.bufferedAmount(p_id);
+}
+
+function _godot_js_websocket_close(p_id, p_code, p_reason) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(146, 1, p_id, p_code, p_reason);
+ const code = p_code;
+ const reason = GodotRuntime.parseString(p_reason);
+ GodotWebSocket.close(p_id, code, reason);
+}
+
+function _godot_js_websocket_create(p_ref, p_url, p_proto, p_on_open, p_on_message, p_on_error, p_on_close) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(147, 1, p_ref, p_url, p_proto, p_on_open, p_on_message, p_on_error, p_on_close);
+ const on_open = GodotRuntime.get_func(p_on_open).bind(null, p_ref);
+ const on_message = GodotRuntime.get_func(p_on_message).bind(null, p_ref);
+ const on_error = GodotRuntime.get_func(p_on_error).bind(null, p_ref);
+ const on_close = GodotRuntime.get_func(p_on_close).bind(null, p_ref);
+ const url = GodotRuntime.parseString(p_url);
+ const protos = GodotRuntime.parseString(p_proto);
+ let socket = null;
+ try {
+ if (protos) {
+ socket = new WebSocket(url, protos.split(","));
+ } else {
+ socket = new WebSocket(url);
+ }
+ } catch (e) {
+ return 0;
+ }
+ socket.binaryType = "arraybuffer";
+ return GodotWebSocket.create(socket, on_open, on_message, on_error, on_close);
+}
+
+function _godot_js_websocket_destroy(p_id) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(148, 1, p_id);
+ GodotWebSocket.destroy(p_id);
+}
+
+function _godot_js_websocket_send(p_id, p_buf, p_buf_len, p_raw) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(149, 1, p_id, p_buf, p_buf_len, p_raw);
+ const bytes_array = new Uint8Array(p_buf_len);
+ let i = 0;
+ for (i = 0; i < p_buf_len; i++) {
+ bytes_array[i] = GodotRuntime.getHeapValue(p_buf + i, "i8");
+ }
+ let out = bytes_array.buffer;
+ if (!p_raw) {
+ out = new TextDecoder("utf-8").decode(bytes_array);
+ }
+ return GodotWebSocket.send(p_id, out);
+}
+
+var GodotWebGL2 = {};
+
+function _godot_webgl2_glFramebufferTextureMultiviewOVR(target, attachment, texture, level, base_view_index, num_views) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(150, 1, target, attachment, texture, level, base_view_index, num_views);
+ const context = GL.currentContext;
+ if (typeof context.multiviewExt === "undefined") {
+ const ext = context.GLctx.getExtension("OVR_multiview2");
+ if (!ext) {
+ GodotRuntime.error("Trying to call glFramebufferTextureMultiviewOVR() without the OVR_multiview2 extension");
+ return;
+ }
+ context.multiviewExt = ext;
+ }
+ const ext = context.multiviewExt;
+ ext.framebufferTextureMultiviewOVR(target, attachment, GL.textures[texture], level, base_view_index, num_views);
+}
+
+function _godot_webgl2_glGetBufferSubData(target, offset, size, data) {
+ if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(151, 1, target, offset, size, data);
+ const gl_context_handle = _emscripten_webgl_get_current_context();
+ const gl = GL.getContext(gl_context_handle);
+ if (gl) {
+ gl.GLctx["getBufferSubData"](target, offset, GROWABLE_HEAP_U8(), data, size);
+ }
+}
+
+function _setTempRet0(val) {
+ setTempRet0(val);
+}
+
+function __isLeapYear(year) {
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
+}
+
+function __arraySum(array, index) {
+ var sum = 0;
+ for (var i = 0; i <= index; sum += array[i++]) {}
+ return sum;
+}
+
+var __MONTH_DAYS_LEAP = [ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
+
+var __MONTH_DAYS_REGULAR = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
+
+function __addDays(date, days) {
+ var newDate = new Date(date.getTime());
+ while (days > 0) {
+ var leap = __isLeapYear(newDate.getFullYear());
+ var currentMonth = newDate.getMonth();
+ var daysInCurrentMonth = (leap ? __MONTH_DAYS_LEAP : __MONTH_DAYS_REGULAR)[currentMonth];
+ if (days > daysInCurrentMonth - newDate.getDate()) {
+ days -= daysInCurrentMonth - newDate.getDate() + 1;
+ newDate.setDate(1);
+ if (currentMonth < 11) {
+ newDate.setMonth(currentMonth + 1);
+ } else {
+ newDate.setMonth(0);
+ newDate.setFullYear(newDate.getFullYear() + 1);
+ }
+ } else {
+ newDate.setDate(newDate.getDate() + days);
+ return newDate;
+ }
+ }
+ return newDate;
+}
+
+function _strftime(s, maxsize, format, tm) {
+ var tm_zone = GROWABLE_HEAP_I32()[tm + 40 >> 2];
+ var date = {
+ tm_sec: GROWABLE_HEAP_I32()[tm >> 2],
+ tm_min: GROWABLE_HEAP_I32()[tm + 4 >> 2],
+ tm_hour: GROWABLE_HEAP_I32()[tm + 8 >> 2],
+ tm_mday: GROWABLE_HEAP_I32()[tm + 12 >> 2],
+ tm_mon: GROWABLE_HEAP_I32()[tm + 16 >> 2],
+ tm_year: GROWABLE_HEAP_I32()[tm + 20 >> 2],
+ tm_wday: GROWABLE_HEAP_I32()[tm + 24 >> 2],
+ tm_yday: GROWABLE_HEAP_I32()[tm + 28 >> 2],
+ tm_isdst: GROWABLE_HEAP_I32()[tm + 32 >> 2],
+ tm_gmtoff: GROWABLE_HEAP_I32()[tm + 36 >> 2],
+ tm_zone: tm_zone ? UTF8ToString(tm_zone) : ""
+ };
+ var pattern = UTF8ToString(format);
+ var EXPANSION_RULES_1 = {
+ "%c": "%a %b %d %H:%M:%S %Y",
+ "%D": "%m/%d/%y",
+ "%F": "%Y-%m-%d",
+ "%h": "%b",
+ "%r": "%I:%M:%S %p",
+ "%R": "%H:%M",
+ "%T": "%H:%M:%S",
+ "%x": "%m/%d/%y",
+ "%X": "%H:%M:%S",
+ "%Ec": "%c",
+ "%EC": "%C",
+ "%Ex": "%m/%d/%y",
+ "%EX": "%H:%M:%S",
+ "%Ey": "%y",
+ "%EY": "%Y",
+ "%Od": "%d",
+ "%Oe": "%e",
+ "%OH": "%H",
+ "%OI": "%I",
+ "%Om": "%m",
+ "%OM": "%M",
+ "%OS": "%S",
+ "%Ou": "%u",
+ "%OU": "%U",
+ "%OV": "%V",
+ "%Ow": "%w",
+ "%OW": "%W",
+ "%Oy": "%y"
+ };
+ for (var rule in EXPANSION_RULES_1) {
+ pattern = pattern.replace(new RegExp(rule, "g"), EXPANSION_RULES_1[rule]);
+ }
+ var WEEKDAYS = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ];
+ var MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];
+ function leadingSomething(value, digits, character) {
+ var str = typeof value == "number" ? value.toString() : value || "";
+ while (str.length < digits) {
+ str = character[0] + str;
+ }
+ return str;
+ }
+ function leadingNulls(value, digits) {
+ return leadingSomething(value, digits, "0");
+ }
+ function compareByDay(date1, date2) {
+ function sgn(value) {
+ return value < 0 ? -1 : value > 0 ? 1 : 0;
+ }
+ var compare;
+ if ((compare = sgn(date1.getFullYear() - date2.getFullYear())) === 0) {
+ if ((compare = sgn(date1.getMonth() - date2.getMonth())) === 0) {
+ compare = sgn(date1.getDate() - date2.getDate());
+ }
+ }
+ return compare;
+ }
+ function getFirstWeekStartDate(janFourth) {
+ switch (janFourth.getDay()) {
+ case 0:
+ return new Date(janFourth.getFullYear() - 1, 11, 29);
+
+ case 1:
+ return janFourth;
+
+ case 2:
+ return new Date(janFourth.getFullYear(), 0, 3);
+
+ case 3:
+ return new Date(janFourth.getFullYear(), 0, 2);
+
+ case 4:
+ return new Date(janFourth.getFullYear(), 0, 1);
+
+ case 5:
+ return new Date(janFourth.getFullYear() - 1, 11, 31);
+
+ case 6:
+ return new Date(janFourth.getFullYear() - 1, 11, 30);
+ }
+ }
+ function getWeekBasedYear(date) {
+ var thisDate = __addDays(new Date(date.tm_year + 1900, 0, 1), date.tm_yday);
+ var janFourthThisYear = new Date(thisDate.getFullYear(), 0, 4);
+ var janFourthNextYear = new Date(thisDate.getFullYear() + 1, 0, 4);
+ var firstWeekStartThisYear = getFirstWeekStartDate(janFourthThisYear);
+ var firstWeekStartNextYear = getFirstWeekStartDate(janFourthNextYear);
+ if (compareByDay(firstWeekStartThisYear, thisDate) <= 0) {
+ if (compareByDay(firstWeekStartNextYear, thisDate) <= 0) {
+ return thisDate.getFullYear() + 1;
+ } else {
+ return thisDate.getFullYear();
+ }
+ } else {
+ return thisDate.getFullYear() - 1;
+ }
+ }
+ var EXPANSION_RULES_2 = {
+ "%a": function(date) {
+ return WEEKDAYS[date.tm_wday].substring(0, 3);
+ },
+ "%A": function(date) {
+ return WEEKDAYS[date.tm_wday];
+ },
+ "%b": function(date) {
+ return MONTHS[date.tm_mon].substring(0, 3);
+ },
+ "%B": function(date) {
+ return MONTHS[date.tm_mon];
+ },
+ "%C": function(date) {
+ var year = date.tm_year + 1900;
+ return leadingNulls(year / 100 | 0, 2);
+ },
+ "%d": function(date) {
+ return leadingNulls(date.tm_mday, 2);
+ },
+ "%e": function(date) {
+ return leadingSomething(date.tm_mday, 2, " ");
+ },
+ "%g": function(date) {
+ return getWeekBasedYear(date).toString().substring(2);
+ },
+ "%G": function(date) {
+ return getWeekBasedYear(date);
+ },
+ "%H": function(date) {
+ return leadingNulls(date.tm_hour, 2);
+ },
+ "%I": function(date) {
+ var twelveHour = date.tm_hour;
+ if (twelveHour == 0) twelveHour = 12; else if (twelveHour > 12) twelveHour -= 12;
+ return leadingNulls(twelveHour, 2);
+ },
+ "%j": function(date) {
+ return leadingNulls(date.tm_mday + __arraySum(__isLeapYear(date.tm_year + 1900) ? __MONTH_DAYS_LEAP : __MONTH_DAYS_REGULAR, date.tm_mon - 1), 3);
+ },
+ "%m": function(date) {
+ return leadingNulls(date.tm_mon + 1, 2);
+ },
+ "%M": function(date) {
+ return leadingNulls(date.tm_min, 2);
+ },
+ "%n": function() {
+ return "\n";
+ },
+ "%p": function(date) {
+ if (date.tm_hour >= 0 && date.tm_hour < 12) {
+ return "AM";
+ } else {
+ return "PM";
+ }
+ },
+ "%S": function(date) {
+ return leadingNulls(date.tm_sec, 2);
+ },
+ "%t": function() {
+ return "\t";
+ },
+ "%u": function(date) {
+ return date.tm_wday || 7;
+ },
+ "%U": function(date) {
+ var janFirst = new Date(date.tm_year + 1900, 0, 1);
+ var firstSunday = janFirst.getDay() === 0 ? janFirst : __addDays(janFirst, 7 - janFirst.getDay());
+ var endDate = new Date(date.tm_year + 1900, date.tm_mon, date.tm_mday);
+ if (compareByDay(firstSunday, endDate) < 0) {
+ var februaryFirstUntilEndMonth = __arraySum(__isLeapYear(endDate.getFullYear()) ? __MONTH_DAYS_LEAP : __MONTH_DAYS_REGULAR, endDate.getMonth() - 1) - 31;
+ var firstSundayUntilEndJanuary = 31 - firstSunday.getDate();
+ var days = firstSundayUntilEndJanuary + februaryFirstUntilEndMonth + endDate.getDate();
+ return leadingNulls(Math.ceil(days / 7), 2);
+ }
+ return compareByDay(firstSunday, janFirst) === 0 ? "01" : "00";
+ },
+ "%V": function(date) {
+ var janFourthThisYear = new Date(date.tm_year + 1900, 0, 4);
+ var janFourthNextYear = new Date(date.tm_year + 1901, 0, 4);
+ var firstWeekStartThisYear = getFirstWeekStartDate(janFourthThisYear);
+ var firstWeekStartNextYear = getFirstWeekStartDate(janFourthNextYear);
+ var endDate = __addDays(new Date(date.tm_year + 1900, 0, 1), date.tm_yday);
+ if (compareByDay(endDate, firstWeekStartThisYear) < 0) {
+ return "53";
+ }
+ if (compareByDay(firstWeekStartNextYear, endDate) <= 0) {
+ return "01";
+ }
+ var daysDifference;
+ if (firstWeekStartThisYear.getFullYear() < date.tm_year + 1900) {
+ daysDifference = date.tm_yday + 32 - firstWeekStartThisYear.getDate();
+ } else {
+ daysDifference = date.tm_yday + 1 - firstWeekStartThisYear.getDate();
+ }
+ return leadingNulls(Math.ceil(daysDifference / 7), 2);
+ },
+ "%w": function(date) {
+ return date.tm_wday;
+ },
+ "%W": function(date) {
+ var janFirst = new Date(date.tm_year, 0, 1);
+ var firstMonday = janFirst.getDay() === 1 ? janFirst : __addDays(janFirst, janFirst.getDay() === 0 ? 1 : 7 - janFirst.getDay() + 1);
+ var endDate = new Date(date.tm_year + 1900, date.tm_mon, date.tm_mday);
+ if (compareByDay(firstMonday, endDate) < 0) {
+ var februaryFirstUntilEndMonth = __arraySum(__isLeapYear(endDate.getFullYear()) ? __MONTH_DAYS_LEAP : __MONTH_DAYS_REGULAR, endDate.getMonth() - 1) - 31;
+ var firstMondayUntilEndJanuary = 31 - firstMonday.getDate();
+ var days = firstMondayUntilEndJanuary + februaryFirstUntilEndMonth + endDate.getDate();
+ return leadingNulls(Math.ceil(days / 7), 2);
+ }
+ return compareByDay(firstMonday, janFirst) === 0 ? "01" : "00";
+ },
+ "%y": function(date) {
+ return (date.tm_year + 1900).toString().substring(2);
+ },
+ "%Y": function(date) {
+ return date.tm_year + 1900;
+ },
+ "%z": function(date) {
+ var off = date.tm_gmtoff;
+ var ahead = off >= 0;
+ off = Math.abs(off) / 60;
+ off = off / 60 * 100 + off % 60;
+ return (ahead ? "+" : "-") + String("0000" + off).slice(-4);
+ },
+ "%Z": function(date) {
+ return date.tm_zone;
+ },
+ "%%": function() {
+ return "%";
+ }
+ };
+ pattern = pattern.replace(/%%/g, "\0\0");
+ for (var rule in EXPANSION_RULES_2) {
+ if (pattern.includes(rule)) {
+ pattern = pattern.replace(new RegExp(rule, "g"), EXPANSION_RULES_2[rule](date));
+ }
+ }
+ pattern = pattern.replace(/\0\0/g, "%");
+ var bytes = intArrayFromString(pattern, false);
+ if (bytes.length > maxsize) {
+ return 0;
+ }
+ writeArrayToMemory(bytes, s);
+ return bytes.length - 1;
+}
+
+function _strftime_l(s, maxsize, format, tm) {
+ return _strftime(s, maxsize, format, tm);
+}
+
+function _time(ptr) {
+ var ret = Date.now() / 1e3 | 0;
+ if (ptr) {
+ GROWABLE_HEAP_I32()[ptr >> 2] = ret;
+ }
+ return ret;
+}
+
+PThread.init();
+
+var FSNode = function(parent, name, mode, rdev) {
+ if (!parent) {
+ parent = this;
+ }
+ this.parent = parent;
+ this.mount = parent.mount;
+ this.mounted = null;
+ this.id = FS.nextInode++;
+ this.name = name;
+ this.mode = mode;
+ this.node_ops = {};
+ this.stream_ops = {};
+ this.rdev = rdev;
+};
+
+var readMode = 292 | 73;
+
+var writeMode = 146;
+
+Object.defineProperties(FSNode.prototype, {
+ read: {
+ get: function() {
+ return (this.mode & readMode) === readMode;
+ },
+ set: function(val) {
+ val ? this.mode |= readMode : this.mode &= ~readMode;
+ }
+ },
+ write: {
+ get: function() {
+ return (this.mode & writeMode) === writeMode;
+ },
+ set: function(val) {
+ val ? this.mode |= writeMode : this.mode &= ~writeMode;
+ }
+ },
+ isFolder: {
+ get: function() {
+ return FS.isDir(this.mode);
+ }
+ },
+ isDevice: {
+ get: function() {
+ return FS.isChrdev(this.mode);
+ }
+ }
+});
+
+FS.FSNode = FSNode;
+
+FS.staticInit();
+
+ERRNO_CODES = {
+ "EPERM": 63,
+ "ENOENT": 44,
+ "ESRCH": 71,
+ "EINTR": 27,
+ "EIO": 29,
+ "ENXIO": 60,
+ "E2BIG": 1,
+ "ENOEXEC": 45,
+ "EBADF": 8,
+ "ECHILD": 12,
+ "EAGAIN": 6,
+ "EWOULDBLOCK": 6,
+ "ENOMEM": 48,
+ "EACCES": 2,
+ "EFAULT": 21,
+ "ENOTBLK": 105,
+ "EBUSY": 10,
+ "EEXIST": 20,
+ "EXDEV": 75,
+ "ENODEV": 43,
+ "ENOTDIR": 54,
+ "EISDIR": 31,
+ "EINVAL": 28,
+ "ENFILE": 41,
+ "EMFILE": 33,
+ "ENOTTY": 59,
+ "ETXTBSY": 74,
+ "EFBIG": 22,
+ "ENOSPC": 51,
+ "ESPIPE": 70,
+ "EROFS": 69,
+ "EMLINK": 34,
+ "EPIPE": 64,
+ "EDOM": 18,
+ "ERANGE": 68,
+ "ENOMSG": 49,
+ "EIDRM": 24,
+ "ECHRNG": 106,
+ "EL2NSYNC": 156,
+ "EL3HLT": 107,
+ "EL3RST": 108,
+ "ELNRNG": 109,
+ "EUNATCH": 110,
+ "ENOCSI": 111,
+ "EL2HLT": 112,
+ "EDEADLK": 16,
+ "ENOLCK": 46,
+ "EBADE": 113,
+ "EBADR": 114,
+ "EXFULL": 115,
+ "ENOANO": 104,
+ "EBADRQC": 103,
+ "EBADSLT": 102,
+ "EDEADLOCK": 16,
+ "EBFONT": 101,
+ "ENOSTR": 100,
+ "ENODATA": 116,
+ "ETIME": 117,
+ "ENOSR": 118,
+ "ENONET": 119,
+ "ENOPKG": 120,
+ "EREMOTE": 121,
+ "ENOLINK": 47,
+ "EADV": 122,
+ "ESRMNT": 123,
+ "ECOMM": 124,
+ "EPROTO": 65,
+ "EMULTIHOP": 36,
+ "EDOTDOT": 125,
+ "EBADMSG": 9,
+ "ENOTUNIQ": 126,
+ "EBADFD": 127,
+ "EREMCHG": 128,
+ "ELIBACC": 129,
+ "ELIBBAD": 130,
+ "ELIBSCN": 131,
+ "ELIBMAX": 132,
+ "ELIBEXEC": 133,
+ "ENOSYS": 52,
+ "ENOTEMPTY": 55,
+ "ENAMETOOLONG": 37,
+ "ELOOP": 32,
+ "EOPNOTSUPP": 138,
+ "EPFNOSUPPORT": 139,
+ "ECONNRESET": 15,
+ "ENOBUFS": 42,
+ "EAFNOSUPPORT": 5,
+ "EPROTOTYPE": 67,
+ "ENOTSOCK": 57,
+ "ENOPROTOOPT": 50,
+ "ESHUTDOWN": 140,
+ "ECONNREFUSED": 14,
+ "EADDRINUSE": 3,
+ "ECONNABORTED": 13,
+ "ENETUNREACH": 40,
+ "ENETDOWN": 38,
+ "ETIMEDOUT": 73,
+ "EHOSTDOWN": 142,
+ "EHOSTUNREACH": 23,
+ "EINPROGRESS": 26,
+ "EALREADY": 7,
+ "EDESTADDRREQ": 17,
+ "EMSGSIZE": 35,
+ "EPROTONOSUPPORT": 66,
+ "ESOCKTNOSUPPORT": 137,
+ "EADDRNOTAVAIL": 4,
+ "ENETRESET": 39,
+ "EISCONN": 30,
+ "ENOTCONN": 53,
+ "ETOOMANYREFS": 141,
+ "EUSERS": 136,
+ "EDQUOT": 19,
+ "ESTALE": 72,
+ "ENOTSUP": 138,
+ "ENOMEDIUM": 148,
+ "EILSEQ": 25,
+ "EOVERFLOW": 61,
+ "ECANCELED": 11,
+ "ENOTRECOVERABLE": 56,
+ "EOWNERDEAD": 62,
+ "ESTRPIPE": 135
+};
+
+Module["requestFullscreen"] = function Module_requestFullscreen(lockPointer, resizeCanvas) {
+ Browser.requestFullscreen(lockPointer, resizeCanvas);
+};
+
+Module["requestFullScreen"] = function Module_requestFullScreen() {
+ Browser.requestFullScreen();
+};
+
+Module["requestAnimationFrame"] = function Module_requestAnimationFrame(func) {
+ Browser.requestAnimationFrame(func);
+};
+
+Module["setCanvasSize"] = function Module_setCanvasSize(width, height, noUpdates) {
+ Browser.setCanvasSize(width, height, noUpdates);
+};
+
+Module["pauseMainLoop"] = function Module_pauseMainLoop() {
+ Browser.mainLoop.pause();
+};
+
+Module["resumeMainLoop"] = function Module_resumeMainLoop() {
+ Browser.mainLoop.resume();
+};
+
+Module["getUserMedia"] = function Module_getUserMedia() {
+ Browser.getUserMedia();
+};
+
+Module["createContext"] = function Module_createContext(canvas, useWebGL, setInModule, webGLContextAttributes) {
+ return Browser.createContext(canvas, useWebGL, setInModule, webGLContextAttributes);
+};
+
+var GLctx;
+
+var __miniTempWebGLIntBuffersStorage = new Int32Array(288);
+
+for (var i = 0; i < 288; ++i) {
+ __miniTempWebGLIntBuffers[i] = __miniTempWebGLIntBuffersStorage.subarray(0, i + 1);
+}
+
+var miniTempWebGLFloatBuffersStorage = new Float32Array(288);
+
+for (var i = 0; i < 288; ++i) {
+ miniTempWebGLFloatBuffers[i] = miniTempWebGLFloatBuffersStorage.subarray(0, i + 1);
+}
+
+Module["request_quit"] = function() {
+ GodotOS.request_quit();
+};
+
+Module["onExit"] = GodotOS.cleanup;
+
+GodotOS._fs_sync_promise = Promise.resolve();
+
+Module["initConfig"] = GodotConfig.init_config;
+
+Module["initFS"] = GodotFS.init;
+
+Module["copyToFS"] = GodotFS.copy_to_fs;
+
+GodotOS.atexit(function(resolve, reject) {
+ GodotDisplayCursor.clear();
+ resolve();
+});
+
+GodotOS.atexit(function(resolve, reject) {
+ GodotEventListeners.clear();
+ resolve();
+});
+
+GodotOS.atexit(function(resolve, reject) {
+ GodotDisplayVK.clear();
+ resolve();
+});
+
+var proxiedFunctionTable = [ null, exitOnMainThread, ___syscall__newselect, ___syscall_accept4, ___syscall_bind, ___syscall_chdir, ___syscall_chmod, ___syscall_connect, ___syscall_faccessat, ___syscall_fchmod, ___syscall_fcntl64, ___syscall_fstatat64, ___syscall_getcwd, ___syscall_getdents64, ___syscall_getsockname, ___syscall_getsockopt, ___syscall_ioctl, ___syscall_listen, ___syscall_lstat64, ___syscall_mkdir, ___syscall_open, ___syscall_poll, ___syscall_readlink, ___syscall_recvfrom, ___syscall_rename, ___syscall_rmdir, ___syscall_sendto, ___syscall_socket, ___syscall_stat64, ___syscall_statfs64, ___syscall_symlink, ___syscall_unlink, _tzset_impl, _emscripten_force_exit, _emscripten_set_canvas_element_size_main_thread, _emscripten_webgl_destroy_context, _emscripten_webgl_enable_extension, _environ_get, _environ_sizes_get, _fd_close, _fd_fdstat_get, _fd_read, _fd_seek, _fd_write, _getaddrinfo, _godot_audio_has_worklet, _godot_audio_init, _godot_audio_input_start, _godot_audio_input_stop, _godot_audio_is_available, _godot_audio_resume, _godot_audio_worklet_create, _godot_audio_worklet_start, _godot_js_config_canvas_id_get, _godot_js_config_locale_get, _godot_js_display_alert, _godot_js_display_canvas_focus, _godot_js_display_canvas_is_focused, _godot_js_display_clipboard_get, _godot_js_display_clipboard_set, _godot_js_display_cursor_is_hidden, _godot_js_display_cursor_is_locked, _godot_js_display_cursor_lock_set, _godot_js_display_cursor_set_custom_shape, _godot_js_display_cursor_set_shape, _godot_js_display_cursor_set_visible, _godot_js_display_desired_size_set, _godot_js_display_fullscreen_cb, _godot_js_display_fullscreen_exit, _godot_js_display_fullscreen_request, _godot_js_display_has_webgl, _godot_js_display_is_swap_ok_cancel, _godot_js_display_notification_cb, _godot_js_display_pixel_ratio_get, _godot_js_display_screen_dpi_get, _godot_js_display_screen_size_get, _godot_js_display_setup_canvas, _godot_js_display_size_update, _godot_js_display_touchscreen_is_available, _godot_js_display_tts_available, _godot_js_display_vk_available, _godot_js_display_vk_cb, _godot_js_display_vk_hide, _godot_js_display_vk_show, _godot_js_display_window_blur_cb, _godot_js_display_window_icon_set, _godot_js_display_window_size_get, _godot_js_display_window_title_set, _godot_js_fetch_create, _godot_js_fetch_free, _godot_js_fetch_http_status_get, _godot_js_fetch_is_chunked, _godot_js_fetch_read_chunk, _godot_js_fetch_read_headers, _godot_js_fetch_state_get, _godot_js_input_drop_files_cb, _godot_js_input_gamepad_cb, _godot_js_input_gamepad_sample, _godot_js_input_gamepad_sample_count, _godot_js_input_gamepad_sample_get, _godot_js_input_key_cb, _godot_js_input_mouse_button_cb, _godot_js_input_mouse_move_cb, _godot_js_input_mouse_wheel_cb, _godot_js_input_paste_cb, _godot_js_input_touch_cb, _godot_js_input_vibrate_handheld, _godot_js_os_download_buffer, _godot_js_os_execute, _godot_js_os_finish_async, _godot_js_os_fs_is_persistent, _godot_js_os_fs_sync, _godot_js_os_has_feature, _godot_js_os_hw_concurrency_get, _godot_js_os_request_quit_cb, _godot_js_os_shell_open, _godot_js_pwa_cb, _godot_js_pwa_update, _godot_js_rtc_datachannel_close, _godot_js_rtc_datachannel_connect, _godot_js_rtc_datachannel_destroy, _godot_js_rtc_datachannel_get_buffered_amount, _godot_js_rtc_datachannel_id_get, _godot_js_rtc_datachannel_is_negotiated, _godot_js_rtc_datachannel_is_ordered, _godot_js_rtc_datachannel_label_get, _godot_js_rtc_datachannel_max_packet_lifetime_get, _godot_js_rtc_datachannel_max_retransmits_get, _godot_js_rtc_datachannel_ready_state_get, _godot_js_rtc_datachannel_send, _godot_js_rtc_pc_close, _godot_js_rtc_pc_create, _godot_js_rtc_pc_datachannel_create, _godot_js_rtc_pc_destroy, _godot_js_rtc_pc_ice_candidate_add, _godot_js_rtc_pc_local_description_set, _godot_js_rtc_pc_offer_create, _godot_js_rtc_pc_remote_description_set, _godot_js_tts_get_voices, _godot_js_tts_is_paused, _godot_js_tts_is_speaking, _godot_js_tts_pause, _godot_js_tts_resume, _godot_js_tts_speak, _godot_js_tts_stop, _godot_js_websocket_buffered_amount, _godot_js_websocket_close, _godot_js_websocket_create, _godot_js_websocket_destroy, _godot_js_websocket_send, _godot_webgl2_glFramebufferTextureMultiviewOVR, _godot_webgl2_glGetBufferSubData ];
+
+var ASSERTIONS = true;
+
+function intArrayFromString(stringy, dontAddNull, length) {
+ var len = length > 0 ? length : lengthBytesUTF8(stringy) + 1;
+ var u8array = new Array(len);
+ var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length);
+ if (dontAddNull) u8array.length = numBytesWritten;
+ return u8array;
+}
+
+function intArrayToString(array) {
+ var ret = [];
+ for (var i = 0; i < array.length; i++) {
+ var chr = array[i];
+ if (chr > 255) {
+ if (ASSERTIONS) {
+ assert(false, "Character code " + chr + " (" + String.fromCharCode(chr) + ") at offset " + i + " not in 0x00-0xFF.");
+ }
+ chr &= 255;
+ }
+ ret.push(String.fromCharCode(chr));
+ }
+ return ret.join("");
+}
+
+var asmLibraryArg = {
+ "__assert_fail": ___assert_fail,
+ "__call_sighandler": ___call_sighandler,
+ "__clock_gettime": ___clock_gettime,
+ "__emscripten_init_main_thread_js": ___emscripten_init_main_thread_js,
+ "__emscripten_thread_cleanup": ___emscripten_thread_cleanup,
+ "__pthread_create_js": ___pthread_create_js,
+ "__syscall__newselect": ___syscall__newselect,
+ "__syscall_accept4": ___syscall_accept4,
+ "__syscall_bind": ___syscall_bind,
+ "__syscall_chdir": ___syscall_chdir,
+ "__syscall_chmod": ___syscall_chmod,
+ "__syscall_connect": ___syscall_connect,
+ "__syscall_faccessat": ___syscall_faccessat,
+ "__syscall_fchmod": ___syscall_fchmod,
+ "__syscall_fcntl64": ___syscall_fcntl64,
+ "__syscall_fstatat64": ___syscall_fstatat64,
+ "__syscall_getcwd": ___syscall_getcwd,
+ "__syscall_getdents64": ___syscall_getdents64,
+ "__syscall_getsockname": ___syscall_getsockname,
+ "__syscall_getsockopt": ___syscall_getsockopt,
+ "__syscall_ioctl": ___syscall_ioctl,
+ "__syscall_listen": ___syscall_listen,
+ "__syscall_lstat64": ___syscall_lstat64,
+ "__syscall_mkdir": ___syscall_mkdir,
+ "__syscall_open": ___syscall_open,
+ "__syscall_poll": ___syscall_poll,
+ "__syscall_readlink": ___syscall_readlink,
+ "__syscall_recvfrom": ___syscall_recvfrom,
+ "__syscall_rename": ___syscall_rename,
+ "__syscall_rmdir": ___syscall_rmdir,
+ "__syscall_sendto": ___syscall_sendto,
+ "__syscall_socket": ___syscall_socket,
+ "__syscall_stat64": ___syscall_stat64,
+ "__syscall_statfs64": ___syscall_statfs64,
+ "__syscall_symlink": ___syscall_symlink,
+ "__syscall_unlink": ___syscall_unlink,
+ "_dlopen_js": __dlopen_js,
+ "_dlsym_js": __dlsym_js,
+ "_emscripten_default_pthread_stack_size": __emscripten_default_pthread_stack_size,
+ "_emscripten_notify_thread_queue": __emscripten_notify_thread_queue,
+ "_emscripten_proxied_gl_context_activated_from_main_browser_thread": __emscripten_proxied_gl_context_activated_from_main_browser_thread,
+ "_emscripten_throw_longjmp": __emscripten_throw_longjmp,
+ "_gmtime_js": __gmtime_js,
+ "_localtime_js": __localtime_js,
+ "_tzset_js": __tzset_js,
+ "abort": _abort,
+ "clock_gettime": _clock_gettime,
+ "emscripten_cancel_main_loop": _emscripten_cancel_main_loop,
+ "emscripten_check_blocking_allowed": _emscripten_check_blocking_allowed,
+ "emscripten_console_error": _emscripten_console_error,
+ "emscripten_force_exit": _emscripten_force_exit,
+ "emscripten_get_now": _emscripten_get_now,
+ "emscripten_glActiveTexture": _emscripten_glActiveTexture,
+ "emscripten_glAttachShader": _emscripten_glAttachShader,
+ "emscripten_glBeginTransformFeedback": _emscripten_glBeginTransformFeedback,
+ "emscripten_glBindBuffer": _emscripten_glBindBuffer,
+ "emscripten_glBindBufferBase": _emscripten_glBindBufferBase,
+ "emscripten_glBindBufferRange": _emscripten_glBindBufferRange,
+ "emscripten_glBindFramebuffer": _emscripten_glBindFramebuffer,
+ "emscripten_glBindRenderbuffer": _emscripten_glBindRenderbuffer,
+ "emscripten_glBindTexture": _emscripten_glBindTexture,
+ "emscripten_glBindVertexArray": _emscripten_glBindVertexArray,
+ "emscripten_glBlendColor": _emscripten_glBlendColor,
+ "emscripten_glBlendEquation": _emscripten_glBlendEquation,
+ "emscripten_glBlendFunc": _emscripten_glBlendFunc,
+ "emscripten_glBlendFuncSeparate": _emscripten_glBlendFuncSeparate,
+ "emscripten_glBlitFramebuffer": _emscripten_glBlitFramebuffer,
+ "emscripten_glBufferData": _emscripten_glBufferData,
+ "emscripten_glBufferSubData": _emscripten_glBufferSubData,
+ "emscripten_glCheckFramebufferStatus": _emscripten_glCheckFramebufferStatus,
+ "emscripten_glClear": _emscripten_glClear,
+ "emscripten_glClearBufferfv": _emscripten_glClearBufferfv,
+ "emscripten_glClearColor": _emscripten_glClearColor,
+ "emscripten_glClearDepthf": _emscripten_glClearDepthf,
+ "emscripten_glColorMask": _emscripten_glColorMask,
+ "emscripten_glCompileShader": _emscripten_glCompileShader,
+ "emscripten_glCompressedTexImage2D": _emscripten_glCompressedTexImage2D,
+ "emscripten_glCopyBufferSubData": _emscripten_glCopyBufferSubData,
+ "emscripten_glCreateProgram": _emscripten_glCreateProgram,
+ "emscripten_glCreateShader": _emscripten_glCreateShader,
+ "emscripten_glCullFace": _emscripten_glCullFace,
+ "emscripten_glDeleteBuffers": _emscripten_glDeleteBuffers,
+ "emscripten_glDeleteFramebuffers": _emscripten_glDeleteFramebuffers,
+ "emscripten_glDeleteProgram": _emscripten_glDeleteProgram,
+ "emscripten_glDeleteQueries": _emscripten_glDeleteQueries,
+ "emscripten_glDeleteRenderbuffers": _emscripten_glDeleteRenderbuffers,
+ "emscripten_glDeleteShader": _emscripten_glDeleteShader,
+ "emscripten_glDeleteSync": _emscripten_glDeleteSync,
+ "emscripten_glDeleteTextures": _emscripten_glDeleteTextures,
+ "emscripten_glDeleteVertexArrays": _emscripten_glDeleteVertexArrays,
+ "emscripten_glDepthFunc": _emscripten_glDepthFunc,
+ "emscripten_glDepthMask": _emscripten_glDepthMask,
+ "emscripten_glDisable": _emscripten_glDisable,
+ "emscripten_glDisableVertexAttribArray": _emscripten_glDisableVertexAttribArray,
+ "emscripten_glDrawArrays": _emscripten_glDrawArrays,
+ "emscripten_glDrawArraysInstanced": _emscripten_glDrawArraysInstanced,
+ "emscripten_glDrawElements": _emscripten_glDrawElements,
+ "emscripten_glDrawElementsInstanced": _emscripten_glDrawElementsInstanced,
+ "emscripten_glEnable": _emscripten_glEnable,
+ "emscripten_glEnableVertexAttribArray": _emscripten_glEnableVertexAttribArray,
+ "emscripten_glEndTransformFeedback": _emscripten_glEndTransformFeedback,
+ "emscripten_glFenceSync": _emscripten_glFenceSync,
+ "emscripten_glFinish": _emscripten_glFinish,
+ "emscripten_glFramebufferRenderbuffer": _emscripten_glFramebufferRenderbuffer,
+ "emscripten_glFramebufferTexture2D": _emscripten_glFramebufferTexture2D,
+ "emscripten_glFramebufferTextureLayer": _emscripten_glFramebufferTextureLayer,
+ "emscripten_glFrontFace": _emscripten_glFrontFace,
+ "emscripten_glGenBuffers": _emscripten_glGenBuffers,
+ "emscripten_glGenFramebuffers": _emscripten_glGenFramebuffers,
+ "emscripten_glGenQueries": _emscripten_glGenQueries,
+ "emscripten_glGenRenderbuffers": _emscripten_glGenRenderbuffers,
+ "emscripten_glGenTextures": _emscripten_glGenTextures,
+ "emscripten_glGenVertexArrays": _emscripten_glGenVertexArrays,
+ "emscripten_glGenerateMipmap": _emscripten_glGenerateMipmap,
+ "emscripten_glGetFloatv": _emscripten_glGetFloatv,
+ "emscripten_glGetInteger64v": _emscripten_glGetInteger64v,
+ "emscripten_glGetProgramInfoLog": _emscripten_glGetProgramInfoLog,
+ "emscripten_glGetProgramiv": _emscripten_glGetProgramiv,
+ "emscripten_glGetShaderInfoLog": _emscripten_glGetShaderInfoLog,
+ "emscripten_glGetShaderiv": _emscripten_glGetShaderiv,
+ "emscripten_glGetString": _emscripten_glGetString,
+ "emscripten_glGetStringi": _emscripten_glGetStringi,
+ "emscripten_glGetSynciv": _emscripten_glGetSynciv,
+ "emscripten_glGetUniformBlockIndex": _emscripten_glGetUniformBlockIndex,
+ "emscripten_glGetUniformLocation": _emscripten_glGetUniformLocation,
+ "emscripten_glLinkProgram": _emscripten_glLinkProgram,
+ "emscripten_glPixelStorei": _emscripten_glPixelStorei,
+ "emscripten_glReadBuffer": _emscripten_glReadBuffer,
+ "emscripten_glReadPixels": _emscripten_glReadPixels,
+ "emscripten_glRenderbufferStorage": _emscripten_glRenderbufferStorage,
+ "emscripten_glScissor": _emscripten_glScissor,
+ "emscripten_glShaderSource": _emscripten_glShaderSource,
+ "emscripten_glTexImage2D": _emscripten_glTexImage2D,
+ "emscripten_glTexImage3D": _emscripten_glTexImage3D,
+ "emscripten_glTexParameterf": _emscripten_glTexParameterf,
+ "emscripten_glTexParameteri": _emscripten_glTexParameteri,
+ "emscripten_glTexStorage2D": _emscripten_glTexStorage2D,
+ "emscripten_glTexSubImage3D": _emscripten_glTexSubImage3D,
+ "emscripten_glTransformFeedbackVaryings": _emscripten_glTransformFeedbackVaryings,
+ "emscripten_glUniform1f": _emscripten_glUniform1f,
+ "emscripten_glUniform1i": _emscripten_glUniform1i,
+ "emscripten_glUniform1iv": _emscripten_glUniform1iv,
+ "emscripten_glUniform1ui": _emscripten_glUniform1ui,
+ "emscripten_glUniform1uiv": _emscripten_glUniform1uiv,
+ "emscripten_glUniform2f": _emscripten_glUniform2f,
+ "emscripten_glUniform2fv": _emscripten_glUniform2fv,
+ "emscripten_glUniform2iv": _emscripten_glUniform2iv,
+ "emscripten_glUniform3fv": _emscripten_glUniform3fv,
+ "emscripten_glUniform4f": _emscripten_glUniform4f,
+ "emscripten_glUniform4fv": _emscripten_glUniform4fv,
+ "emscripten_glUniformBlockBinding": _emscripten_glUniformBlockBinding,
+ "emscripten_glUniformMatrix4fv": _emscripten_glUniformMatrix4fv,
+ "emscripten_glUseProgram": _emscripten_glUseProgram,
+ "emscripten_glVertexAttrib4f": _emscripten_glVertexAttrib4f,
+ "emscripten_glVertexAttribDivisor": _emscripten_glVertexAttribDivisor,
+ "emscripten_glVertexAttribI4ui": _emscripten_glVertexAttribI4ui,
+ "emscripten_glVertexAttribIPointer": _emscripten_glVertexAttribIPointer,
+ "emscripten_glVertexAttribPointer": _emscripten_glVertexAttribPointer,
+ "emscripten_glViewport": _emscripten_glViewport,
+ "emscripten_memcpy_big": _emscripten_memcpy_big,
+ "emscripten_num_logical_cores": _emscripten_num_logical_cores,
+ "emscripten_receive_on_main_thread_js": _emscripten_receive_on_main_thread_js,
+ "emscripten_resize_heap": _emscripten_resize_heap,
+ "emscripten_set_canvas_element_size": _emscripten_set_canvas_element_size,
+ "emscripten_set_main_loop": _emscripten_set_main_loop,
+ "emscripten_supports_offscreencanvas": _emscripten_supports_offscreencanvas,
+ "emscripten_unwind_to_js_event_loop": _emscripten_unwind_to_js_event_loop,
+ "emscripten_webgl_destroy_context": _emscripten_webgl_destroy_context,
+ "emscripten_webgl_do_commit_frame": _emscripten_webgl_do_commit_frame,
+ "emscripten_webgl_do_create_context": _emscripten_webgl_do_create_context,
+ "emscripten_webgl_enable_extension": _emscripten_webgl_enable_extension,
+ "emscripten_webgl_init_context_attributes": _emscripten_webgl_init_context_attributes,
+ "emscripten_webgl_make_context_current_calling_thread": _emscripten_webgl_make_context_current_calling_thread,
+ "environ_get": _environ_get,
+ "environ_sizes_get": _environ_sizes_get,
+ "exit": _exit,
+ "fd_close": _fd_close,
+ "fd_fdstat_get": _fd_fdstat_get,
+ "fd_read": _fd_read,
+ "fd_seek": _fd_seek,
+ "fd_write": _fd_write,
+ "getTempRet0": _getTempRet0,
+ "getaddrinfo": _getaddrinfo,
+ "getnameinfo": _getnameinfo,
+ "gettimeofday": _gettimeofday,
+ "godot_audio_has_worklet": _godot_audio_has_worklet,
+ "godot_audio_init": _godot_audio_init,
+ "godot_audio_input_start": _godot_audio_input_start,
+ "godot_audio_input_stop": _godot_audio_input_stop,
+ "godot_audio_is_available": _godot_audio_is_available,
+ "godot_audio_resume": _godot_audio_resume,
+ "godot_audio_worklet_create": _godot_audio_worklet_create,
+ "godot_audio_worklet_start": _godot_audio_worklet_start,
+ "godot_audio_worklet_state_add": _godot_audio_worklet_state_add,
+ "godot_audio_worklet_state_get": _godot_audio_worklet_state_get,
+ "godot_audio_worklet_state_wait": _godot_audio_worklet_state_wait,
+ "godot_js_config_canvas_id_get": _godot_js_config_canvas_id_get,
+ "godot_js_config_locale_get": _godot_js_config_locale_get,
+ "godot_js_display_alert": _godot_js_display_alert,
+ "godot_js_display_canvas_focus": _godot_js_display_canvas_focus,
+ "godot_js_display_canvas_is_focused": _godot_js_display_canvas_is_focused,
+ "godot_js_display_clipboard_get": _godot_js_display_clipboard_get,
+ "godot_js_display_clipboard_set": _godot_js_display_clipboard_set,
+ "godot_js_display_cursor_is_hidden": _godot_js_display_cursor_is_hidden,
+ "godot_js_display_cursor_is_locked": _godot_js_display_cursor_is_locked,
+ "godot_js_display_cursor_lock_set": _godot_js_display_cursor_lock_set,
+ "godot_js_display_cursor_set_custom_shape": _godot_js_display_cursor_set_custom_shape,
+ "godot_js_display_cursor_set_shape": _godot_js_display_cursor_set_shape,
+ "godot_js_display_cursor_set_visible": _godot_js_display_cursor_set_visible,
+ "godot_js_display_desired_size_set": _godot_js_display_desired_size_set,
+ "godot_js_display_fullscreen_cb": _godot_js_display_fullscreen_cb,
+ "godot_js_display_fullscreen_exit": _godot_js_display_fullscreen_exit,
+ "godot_js_display_fullscreen_request": _godot_js_display_fullscreen_request,
+ "godot_js_display_has_webgl": _godot_js_display_has_webgl,
+ "godot_js_display_is_swap_ok_cancel": _godot_js_display_is_swap_ok_cancel,
+ "godot_js_display_notification_cb": _godot_js_display_notification_cb,
+ "godot_js_display_pixel_ratio_get": _godot_js_display_pixel_ratio_get,
+ "godot_js_display_screen_dpi_get": _godot_js_display_screen_dpi_get,
+ "godot_js_display_screen_size_get": _godot_js_display_screen_size_get,
+ "godot_js_display_setup_canvas": _godot_js_display_setup_canvas,
+ "godot_js_display_size_update": _godot_js_display_size_update,
+ "godot_js_display_touchscreen_is_available": _godot_js_display_touchscreen_is_available,
+ "godot_js_display_tts_available": _godot_js_display_tts_available,
+ "godot_js_display_vk_available": _godot_js_display_vk_available,
+ "godot_js_display_vk_cb": _godot_js_display_vk_cb,
+ "godot_js_display_vk_hide": _godot_js_display_vk_hide,
+ "godot_js_display_vk_show": _godot_js_display_vk_show,
+ "godot_js_display_window_blur_cb": _godot_js_display_window_blur_cb,
+ "godot_js_display_window_icon_set": _godot_js_display_window_icon_set,
+ "godot_js_display_window_size_get": _godot_js_display_window_size_get,
+ "godot_js_display_window_title_set": _godot_js_display_window_title_set,
+ "godot_js_fetch_create": _godot_js_fetch_create,
+ "godot_js_fetch_free": _godot_js_fetch_free,
+ "godot_js_fetch_http_status_get": _godot_js_fetch_http_status_get,
+ "godot_js_fetch_is_chunked": _godot_js_fetch_is_chunked,
+ "godot_js_fetch_read_chunk": _godot_js_fetch_read_chunk,
+ "godot_js_fetch_read_headers": _godot_js_fetch_read_headers,
+ "godot_js_fetch_state_get": _godot_js_fetch_state_get,
+ "godot_js_input_drop_files_cb": _godot_js_input_drop_files_cb,
+ "godot_js_input_gamepad_cb": _godot_js_input_gamepad_cb,
+ "godot_js_input_gamepad_sample": _godot_js_input_gamepad_sample,
+ "godot_js_input_gamepad_sample_count": _godot_js_input_gamepad_sample_count,
+ "godot_js_input_gamepad_sample_get": _godot_js_input_gamepad_sample_get,
+ "godot_js_input_key_cb": _godot_js_input_key_cb,
+ "godot_js_input_mouse_button_cb": _godot_js_input_mouse_button_cb,
+ "godot_js_input_mouse_move_cb": _godot_js_input_mouse_move_cb,
+ "godot_js_input_mouse_wheel_cb": _godot_js_input_mouse_wheel_cb,
+ "godot_js_input_paste_cb": _godot_js_input_paste_cb,
+ "godot_js_input_touch_cb": _godot_js_input_touch_cb,
+ "godot_js_input_vibrate_handheld": _godot_js_input_vibrate_handheld,
+ "godot_js_os_download_buffer": _godot_js_os_download_buffer,
+ "godot_js_os_execute": _godot_js_os_execute,
+ "godot_js_os_finish_async": _godot_js_os_finish_async,
+ "godot_js_os_fs_is_persistent": _godot_js_os_fs_is_persistent,
+ "godot_js_os_fs_sync": _godot_js_os_fs_sync,
+ "godot_js_os_has_feature": _godot_js_os_has_feature,
+ "godot_js_os_hw_concurrency_get": _godot_js_os_hw_concurrency_get,
+ "godot_js_os_request_quit_cb": _godot_js_os_request_quit_cb,
+ "godot_js_os_shell_open": _godot_js_os_shell_open,
+ "godot_js_pwa_cb": _godot_js_pwa_cb,
+ "godot_js_pwa_update": _godot_js_pwa_update,
+ "godot_js_rtc_datachannel_close": _godot_js_rtc_datachannel_close,
+ "godot_js_rtc_datachannel_connect": _godot_js_rtc_datachannel_connect,
+ "godot_js_rtc_datachannel_destroy": _godot_js_rtc_datachannel_destroy,
+ "godot_js_rtc_datachannel_get_buffered_amount": _godot_js_rtc_datachannel_get_buffered_amount,
+ "godot_js_rtc_datachannel_id_get": _godot_js_rtc_datachannel_id_get,
+ "godot_js_rtc_datachannel_is_negotiated": _godot_js_rtc_datachannel_is_negotiated,
+ "godot_js_rtc_datachannel_is_ordered": _godot_js_rtc_datachannel_is_ordered,
+ "godot_js_rtc_datachannel_label_get": _godot_js_rtc_datachannel_label_get,
+ "godot_js_rtc_datachannel_max_packet_lifetime_get": _godot_js_rtc_datachannel_max_packet_lifetime_get,
+ "godot_js_rtc_datachannel_max_retransmits_get": _godot_js_rtc_datachannel_max_retransmits_get,
+ "godot_js_rtc_datachannel_protocol_get": _godot_js_rtc_datachannel_protocol_get,
+ "godot_js_rtc_datachannel_ready_state_get": _godot_js_rtc_datachannel_ready_state_get,
+ "godot_js_rtc_datachannel_send": _godot_js_rtc_datachannel_send,
+ "godot_js_rtc_pc_close": _godot_js_rtc_pc_close,
+ "godot_js_rtc_pc_create": _godot_js_rtc_pc_create,
+ "godot_js_rtc_pc_datachannel_create": _godot_js_rtc_pc_datachannel_create,
+ "godot_js_rtc_pc_destroy": _godot_js_rtc_pc_destroy,
+ "godot_js_rtc_pc_ice_candidate_add": _godot_js_rtc_pc_ice_candidate_add,
+ "godot_js_rtc_pc_local_description_set": _godot_js_rtc_pc_local_description_set,
+ "godot_js_rtc_pc_offer_create": _godot_js_rtc_pc_offer_create,
+ "godot_js_rtc_pc_remote_description_set": _godot_js_rtc_pc_remote_description_set,
+ "godot_js_tts_get_voices": _godot_js_tts_get_voices,
+ "godot_js_tts_is_paused": _godot_js_tts_is_paused,
+ "godot_js_tts_is_speaking": _godot_js_tts_is_speaking,
+ "godot_js_tts_pause": _godot_js_tts_pause,
+ "godot_js_tts_resume": _godot_js_tts_resume,
+ "godot_js_tts_speak": _godot_js_tts_speak,
+ "godot_js_tts_stop": _godot_js_tts_stop,
+ "godot_js_websocket_buffered_amount": _godot_js_websocket_buffered_amount,
+ "godot_js_websocket_close": _godot_js_websocket_close,
+ "godot_js_websocket_create": _godot_js_websocket_create,
+ "godot_js_websocket_destroy": _godot_js_websocket_destroy,
+ "godot_js_websocket_send": _godot_js_websocket_send,
+ "godot_webgl2_glFramebufferTextureMultiviewOVR": _godot_webgl2_glFramebufferTextureMultiviewOVR,
+ "godot_webgl2_glGetBufferSubData": _godot_webgl2_glGetBufferSubData,
+ "invoke_ii": invoke_ii,
+ "invoke_iii": invoke_iii,
+ "invoke_iiii": invoke_iiii,
+ "invoke_iiiii": invoke_iiiii,
+ "invoke_iiiiiii": invoke_iiiiiii,
+ "invoke_vi": invoke_vi,
+ "invoke_vii": invoke_vii,
+ "invoke_viii": invoke_viii,
+ "invoke_viiii": invoke_viiii,
+ "invoke_viiiiiii": invoke_viiiiiii,
+ "memory": wasmMemory || Module["wasmMemory"],
+ "setTempRet0": _setTempRet0,
+ "strftime": _strftime,
+ "strftime_l": _strftime_l,
+ "time": _time
+};
+
+var asm = createWasm();
+
+var ___wasm_call_ctors = Module["___wasm_call_ctors"] = createExportWrapper("__wasm_call_ctors");
+
+var _emscripten_webgl_make_context_current = Module["_emscripten_webgl_make_context_current"] = createExportWrapper("emscripten_webgl_make_context_current");
+
+var _emscripten_webgl_commit_frame = Module["_emscripten_webgl_commit_frame"] = createExportWrapper("emscripten_webgl_commit_frame");
+
+var __Z14godot_web_mainiPPc = Module["__Z14godot_web_mainiPPc"] = createExportWrapper("_Z14godot_web_mainiPPc");
+
+var _main = Module["_main"] = createExportWrapper("main");
+
+var _malloc = Module["_malloc"] = createExportWrapper("malloc");
+
+var _free = Module["_free"] = createExportWrapper("free");
+
+var ___errno_location = Module["___errno_location"] = createExportWrapper("__errno_location");
+
+var _htonl = Module["_htonl"] = createExportWrapper("htonl");
+
+var _htons = Module["_htons"] = createExportWrapper("htons");
+
+var _ntohs = Module["_ntohs"] = createExportWrapper("ntohs");
+
+var _emscripten_tls_init = Module["_emscripten_tls_init"] = createExportWrapper("emscripten_tls_init");
+
+var _emscripten_builtin_memalign = Module["_emscripten_builtin_memalign"] = createExportWrapper("emscripten_builtin_memalign");
+
+var _emscripten_webgl_get_current_context = Module["_emscripten_webgl_get_current_context"] = createExportWrapper("emscripten_webgl_get_current_context");
+
+var _pthread_self = Module["_pthread_self"] = createExportWrapper("pthread_self");
+
+var _emscripten_dispatch_to_thread_ = Module["_emscripten_dispatch_to_thread_"] = createExportWrapper("emscripten_dispatch_to_thread_");
+
+var ___stdio_exit = Module["___stdio_exit"] = createExportWrapper("__stdio_exit");
+
+var ___funcs_on_exit = Module["___funcs_on_exit"] = createExportWrapper("__funcs_on_exit");
+
+var _emscripten_main_thread_process_queued_calls = Module["_emscripten_main_thread_process_queued_calls"] = createExportWrapper("emscripten_main_thread_process_queued_calls");
+
+var ___dl_seterr = Module["___dl_seterr"] = createExportWrapper("__dl_seterr");
+
+var __emscripten_thread_crashed = Module["__emscripten_thread_crashed"] = createExportWrapper("_emscripten_thread_crashed");
+
+var __emscripten_thread_init = Module["__emscripten_thread_init"] = createExportWrapper("_emscripten_thread_init");
+
+var _raise = Module["_raise"] = createExportWrapper("raise");
+
+var _emscripten_current_thread_process_queued_calls = Module["_emscripten_current_thread_process_queued_calls"] = createExportWrapper("emscripten_current_thread_process_queued_calls");
+
+var _emscripten_main_browser_thread_id = Module["_emscripten_main_browser_thread_id"] = createExportWrapper("emscripten_main_browser_thread_id");
+
+var _emscripten_sync_run_in_main_thread_2 = Module["_emscripten_sync_run_in_main_thread_2"] = createExportWrapper("emscripten_sync_run_in_main_thread_2");
+
+var _emscripten_sync_run_in_main_thread_4 = Module["_emscripten_sync_run_in_main_thread_4"] = createExportWrapper("emscripten_sync_run_in_main_thread_4");
+
+var _emscripten_run_in_main_runtime_thread_js = Module["_emscripten_run_in_main_runtime_thread_js"] = createExportWrapper("emscripten_run_in_main_runtime_thread_js");
+
+var _emscripten_stack_get_base = Module["_emscripten_stack_get_base"] = function() {
+ return (_emscripten_stack_get_base = Module["_emscripten_stack_get_base"] = Module["asm"]["emscripten_stack_get_base"]).apply(null, arguments);
+};
+
+var _emscripten_stack_get_end = Module["_emscripten_stack_get_end"] = function() {
+ return (_emscripten_stack_get_end = Module["_emscripten_stack_get_end"] = Module["asm"]["emscripten_stack_get_end"]).apply(null, arguments);
+};
+
+var __emscripten_thread_free_data = Module["__emscripten_thread_free_data"] = createExportWrapper("_emscripten_thread_free_data");
+
+var __emscripten_thread_exit = Module["__emscripten_thread_exit"] = createExportWrapper("_emscripten_thread_exit");
+
+var _setThrew = Module["_setThrew"] = createExportWrapper("setThrew");
+
+var _saveSetjmp = Module["_saveSetjmp"] = createExportWrapper("saveSetjmp");
+
+var _emscripten_stack_init = Module["_emscripten_stack_init"] = function() {
+ return (_emscripten_stack_init = Module["_emscripten_stack_init"] = Module["asm"]["emscripten_stack_init"]).apply(null, arguments);
+};
+
+var _emscripten_stack_set_limits = Module["_emscripten_stack_set_limits"] = function() {
+ return (_emscripten_stack_set_limits = Module["_emscripten_stack_set_limits"] = Module["asm"]["emscripten_stack_set_limits"]).apply(null, arguments);
+};
+
+var _emscripten_stack_get_free = Module["_emscripten_stack_get_free"] = function() {
+ return (_emscripten_stack_get_free = Module["_emscripten_stack_get_free"] = Module["asm"]["emscripten_stack_get_free"]).apply(null, arguments);
+};
+
+var stackSave = Module["stackSave"] = createExportWrapper("stackSave");
+
+var stackRestore = Module["stackRestore"] = createExportWrapper("stackRestore");
+
+var stackAlloc = Module["stackAlloc"] = createExportWrapper("stackAlloc");
+
+var dynCall_vjiii = Module["dynCall_vjiii"] = createExportWrapper("dynCall_vjiii");
+
+var dynCall_vij = Module["dynCall_vij"] = createExportWrapper("dynCall_vij");
+
+var dynCall_ji = Module["dynCall_ji"] = createExportWrapper("dynCall_ji");
+
+var dynCall_viiij = Module["dynCall_viiij"] = createExportWrapper("dynCall_viiij");
+
+var dynCall_iij = Module["dynCall_iij"] = createExportWrapper("dynCall_iij");
+
+var dynCall_viij = Module["dynCall_viij"] = createExportWrapper("dynCall_viij");
+
+var dynCall_jiji = Module["dynCall_jiji"] = createExportWrapper("dynCall_jiji");
+
+var dynCall_viiiiifiijii = Module["dynCall_viiiiifiijii"] = createExportWrapper("dynCall_viiiiifiijii");
+
+var dynCall_viiiiifiiijjii = Module["dynCall_viiiiifiiijjii"] = createExportWrapper("dynCall_viiiiifiiijjii");
+
+var dynCall_viiiiifiiijii = Module["dynCall_viiiiifiiijii"] = createExportWrapper("dynCall_viiiiifiiijii");
+
+var dynCall_viiiiifiiiijjii = Module["dynCall_viiiiifiiiijjii"] = createExportWrapper("dynCall_viiiiifiiiijjii");
+
+var dynCall_jiifff = Module["dynCall_jiifff"] = createExportWrapper("dynCall_jiifff");
+
+var dynCall_vijf = Module["dynCall_vijf"] = createExportWrapper("dynCall_vijf");
+
+var dynCall_jii = Module["dynCall_jii"] = createExportWrapper("dynCall_jii");
+
+var dynCall_vijiii = Module["dynCall_vijiii"] = createExportWrapper("dynCall_vijiii");
+
+var dynCall_vijiiii = Module["dynCall_vijiiii"] = createExportWrapper("dynCall_vijiiii");
+
+var dynCall_vijii = Module["dynCall_vijii"] = createExportWrapper("dynCall_vijii");
+
+var dynCall_iiij = Module["dynCall_iiij"] = createExportWrapper("dynCall_iiij");
+
+var dynCall_viiiiij = Module["dynCall_viiiiij"] = createExportWrapper("dynCall_viiiiij");
+
+var dynCall_viijiiiiiiiii = Module["dynCall_viijiiiiiiiii"] = createExportWrapper("dynCall_viijiiiiiiiii");
+
+var dynCall_viiiiiji = Module["dynCall_viiiiiji"] = createExportWrapper("dynCall_viiiiiji");
+
+var dynCall_viiijii = Module["dynCall_viiijii"] = createExportWrapper("dynCall_viiijii");
+
+var dynCall_jiiiiiiiiii = Module["dynCall_jiiiiiiiiii"] = createExportWrapper("dynCall_jiiiiiiiiii");
+
+var dynCall_jiiiiii = Module["dynCall_jiiiiii"] = createExportWrapper("dynCall_jiiiiii");
+
+var dynCall_jiiiii = Module["dynCall_jiiiii"] = createExportWrapper("dynCall_jiiiii");
+
+var dynCall_jiii = Module["dynCall_jiii"] = createExportWrapper("dynCall_jiii");
+
+var dynCall_jiiiiiii = Module["dynCall_jiiiiiii"] = createExportWrapper("dynCall_jiiiiiii");
+
+var dynCall_jiiiiiiii = Module["dynCall_jiiiiiiii"] = createExportWrapper("dynCall_jiiiiiiii");
+
+var dynCall_vijiiiiiidddd = Module["dynCall_vijiiiiiidddd"] = createExportWrapper("dynCall_vijiiiiiidddd");
+
+var dynCall_jij = Module["dynCall_jij"] = createExportWrapper("dynCall_jij");
+
+var dynCall_jiiii = Module["dynCall_jiiii"] = createExportWrapper("dynCall_jiiii");
+
+var dynCall_jiij = Module["dynCall_jiij"] = createExportWrapper("dynCall_jiij");
+
+var dynCall_jiijiiii = Module["dynCall_jiijiiii"] = createExportWrapper("dynCall_jiijiiii");
+
+var dynCall_jiiji = Module["dynCall_jiiji"] = createExportWrapper("dynCall_jiiji");
+
+var dynCall_jiiiji = Module["dynCall_jiiiji"] = createExportWrapper("dynCall_jiiiji");
+
+var dynCall_jiijii = Module["dynCall_jiijii"] = createExportWrapper("dynCall_jiijii");
+
+var dynCall_iijiiij = Module["dynCall_iijiiij"] = createExportWrapper("dynCall_iijiiij");
+
+var dynCall_jijjjiiiiijii = Module["dynCall_jijjjiiiiijii"] = createExportWrapper("dynCall_jijjjiiiiijii");
+
+var dynCall_jijiiiiifiii = Module["dynCall_jijiiiiifiii"] = createExportWrapper("dynCall_jijiiiiifiii");
+
+var dynCall_viijiiiiiifiii = Module["dynCall_viijiiiiiifiii"] = createExportWrapper("dynCall_viijiiiiiifiii");
+
+var dynCall_viji = Module["dynCall_viji"] = createExportWrapper("dynCall_viji");
+
+var dynCall_viiji = Module["dynCall_viiji"] = createExportWrapper("dynCall_viiji");
+
+var dynCall_viiiij = Module["dynCall_viiiij"] = createExportWrapper("dynCall_viiiij");
+
+var dynCall_vijj = Module["dynCall_vijj"] = createExportWrapper("dynCall_vijj");
+
+var dynCall_vijji = Module["dynCall_vijji"] = createExportWrapper("dynCall_vijji");
+
+var dynCall_vijjii = Module["dynCall_vijjii"] = createExportWrapper("dynCall_vijjii");
+
+var dynCall_fij = Module["dynCall_fij"] = createExportWrapper("dynCall_fij");
+
+var dynCall_vijiffifff = Module["dynCall_vijiffifff"] = createExportWrapper("dynCall_vijiffifff");
+
+var dynCall_vijff = Module["dynCall_vijff"] = createExportWrapper("dynCall_vijff");
+
+var dynCall_vijiffff = Module["dynCall_vijiffff"] = createExportWrapper("dynCall_vijiffff");
+
+var dynCall_vijjf = Module["dynCall_vijjf"] = createExportWrapper("dynCall_vijjf");
+
+var dynCall_vijij = Module["dynCall_vijij"] = createExportWrapper("dynCall_vijij");
+
+var dynCall_vijif = Module["dynCall_vijif"] = createExportWrapper("dynCall_vijif");
+
+var dynCall_vijiiifi = Module["dynCall_vijiiifi"] = createExportWrapper("dynCall_vijiiifi");
+
+var dynCall_vijiifi = Module["dynCall_vijiifi"] = createExportWrapper("dynCall_vijiifi");
+
+var dynCall_vijiif = Module["dynCall_vijiif"] = createExportWrapper("dynCall_vijiif");
+
+var dynCall_vijifi = Module["dynCall_vijifi"] = createExportWrapper("dynCall_vijifi");
+
+var dynCall_vijijiii = Module["dynCall_vijijiii"] = createExportWrapper("dynCall_vijijiii");
+
+var dynCall_vijijiiii = Module["dynCall_vijijiiii"] = createExportWrapper("dynCall_vijijiiii");
+
+var dynCall_vijijiiiff = Module["dynCall_vijijiiiff"] = createExportWrapper("dynCall_vijijiiiff");
+
+var dynCall_vijijii = Module["dynCall_vijijii"] = createExportWrapper("dynCall_vijijii");
+
+var dynCall_vijiijiiiiii = Module["dynCall_vijiijiiiiii"] = createExportWrapper("dynCall_vijiijiiiiii");
+
+var dynCall_vijiiij = Module["dynCall_vijiiij"] = createExportWrapper("dynCall_vijiiij");
+
+var dynCall_vijiiiiiiji = Module["dynCall_vijiiiiiiji"] = createExportWrapper("dynCall_vijiiiiiiji");
+
+var dynCall_vijjj = Module["dynCall_vijjj"] = createExportWrapper("dynCall_vijjj");
+
+var dynCall_vijdddd = Module["dynCall_vijdddd"] = createExportWrapper("dynCall_vijdddd");
+
+var dynCall_vijififi = Module["dynCall_vijififi"] = createExportWrapper("dynCall_vijififi");
+
+var dynCall_iiiij = Module["dynCall_iiiij"] = createExportWrapper("dynCall_iiiij");
+
+var dynCall_iijji = Module["dynCall_iijji"] = createExportWrapper("dynCall_iijji");
+
+var dynCall_viijj = Module["dynCall_viijj"] = createExportWrapper("dynCall_viijj");
+
+var dynCall_viiiiji = Module["dynCall_viiiiji"] = createExportWrapper("dynCall_viiiiji");
+
+var dynCall_dij = Module["dynCall_dij"] = createExportWrapper("dynCall_dij");
+
+var dynCall_vijd = Module["dynCall_vijd"] = createExportWrapper("dynCall_vijd");
+
+var dynCall_ij = Module["dynCall_ij"] = createExportWrapper("dynCall_ij");
+
+var dynCall_viijiiii = Module["dynCall_viijiiii"] = createExportWrapper("dynCall_viijiiii");
+
+var dynCall_viijiii = Module["dynCall_viijiii"] = createExportWrapper("dynCall_viijiii");
+
+var dynCall_iiji = Module["dynCall_iiji"] = createExportWrapper("dynCall_iiji");
+
+var dynCall_iiiijf = Module["dynCall_iiiijf"] = createExportWrapper("dynCall_iiiijf");
+
+var dynCall_vijiiiii = Module["dynCall_vijiiiii"] = createExportWrapper("dynCall_vijiiiii");
+
+var dynCall_viijd = Module["dynCall_viijd"] = createExportWrapper("dynCall_viijd");
+
+var dynCall_diij = Module["dynCall_diij"] = createExportWrapper("dynCall_diij");
+
+var dynCall_viiiji = Module["dynCall_viiiji"] = createExportWrapper("dynCall_viiiji");
+
+var dynCall_viiijj = Module["dynCall_viiijj"] = createExportWrapper("dynCall_viiijj");
+
+var dynCall_viijji = Module["dynCall_viijji"] = createExportWrapper("dynCall_viijji");
+
+var dynCall_jiiij = Module["dynCall_jiiij"] = createExportWrapper("dynCall_jiiij");
+
+var dynCall_viijii = Module["dynCall_viijii"] = createExportWrapper("dynCall_viijii");
+
+var dynCall_jiijjj = Module["dynCall_jiijjj"] = createExportWrapper("dynCall_jiijjj");
+
+var dynCall_jiijj = Module["dynCall_jiijj"] = createExportWrapper("dynCall_jiijj");
+
+var dynCall_viiijiji = Module["dynCall_viiijiji"] = createExportWrapper("dynCall_viiijiji");
+
+var dynCall_viiijjiji = Module["dynCall_viiijjiji"] = createExportWrapper("dynCall_viiijjiji");
+
+var dynCall_viijiji = Module["dynCall_viijiji"] = createExportWrapper("dynCall_viijiji");
+
+var dynCall_iiiiijiii = Module["dynCall_iiiiijiii"] = createExportWrapper("dynCall_iiiiijiii");
+
+var dynCall_iiiiiijd = Module["dynCall_iiiiiijd"] = createExportWrapper("dynCall_iiiiiijd");
+
+var dynCall_diidj = Module["dynCall_diidj"] = createExportWrapper("dynCall_diidj");
+
+var dynCall_viiiijij = Module["dynCall_viiiijij"] = createExportWrapper("dynCall_viiiijij");
+
+var dynCall_viiidjj = Module["dynCall_viiidjj"] = createExportWrapper("dynCall_viiidjj");
+
+var dynCall_viidj = Module["dynCall_viidj"] = createExportWrapper("dynCall_viidj");
+
+var dynCall_iiijj = Module["dynCall_iiijj"] = createExportWrapper("dynCall_iiijj");
+
+var dynCall_jiid = Module["dynCall_jiid"] = createExportWrapper("dynCall_jiid");
+
+var dynCall_viiiiddji = Module["dynCall_viiiiddji"] = createExportWrapper("dynCall_viiiiddji");
+
+var dynCall_vijiiiiiiiii = Module["dynCall_vijiiiiiiiii"] = createExportWrapper("dynCall_vijiiiiiiiii");
+
+var dynCall_vijiiiffi = Module["dynCall_vijiiiffi"] = createExportWrapper("dynCall_vijiiiffi");
+
+var dynCall_vijiiifii = Module["dynCall_vijiiifii"] = createExportWrapper("dynCall_vijiiifii");
+
+var dynCall_viijfii = Module["dynCall_viijfii"] = createExportWrapper("dynCall_viijfii");
+
+var dynCall_viiiiiiiiiiijjjjjjifiiiiii = Module["dynCall_viiiiiiiiiiijjjjjjifiiiiii"] = createExportWrapper("dynCall_viiiiiiiiiiijjjjjjifiiiiii");
+
+var dynCall_vijifff = Module["dynCall_vijifff"] = createExportWrapper("dynCall_vijifff");
+
+var dynCall_fiji = Module["dynCall_fiji"] = createExportWrapper("dynCall_fiji");
+
+var dynCall_vijiiffifffi = Module["dynCall_vijiiffifffi"] = createExportWrapper("dynCall_vijiiffifffi");
+
+var dynCall_iijj = Module["dynCall_iijj"] = createExportWrapper("dynCall_iijj");
+
+var dynCall_iijjfj = Module["dynCall_iijjfj"] = createExportWrapper("dynCall_iijjfj");
+
+var dynCall_vijiji = Module["dynCall_vijiji"] = createExportWrapper("dynCall_vijiji");
+
+var dynCall_jijii = Module["dynCall_jijii"] = createExportWrapper("dynCall_jijii");
+
+var dynCall_vijid = Module["dynCall_vijid"] = createExportWrapper("dynCall_vijid");
+
+var dynCall_vijiiiiii = Module["dynCall_vijiiiiii"] = createExportWrapper("dynCall_vijiiiiii");
+
+var dynCall_vijiff = Module["dynCall_vijiff"] = createExportWrapper("dynCall_vijiff");
+
+var dynCall_vijjjj = Module["dynCall_vijjjj"] = createExportWrapper("dynCall_vijjjj");
+
+var dynCall_vijiiiiiii = Module["dynCall_vijiiiiiii"] = createExportWrapper("dynCall_vijiiiiiii");
+
+var dynCall_jiiifiiiii = Module["dynCall_jiiifiiiii"] = createExportWrapper("dynCall_jiiifiiiii");
+
+var dynCall_viiiifijii = Module["dynCall_viiiifijii"] = createExportWrapper("dynCall_viiiifijii");
+
+var dynCall_viiiifiijjii = Module["dynCall_viiiifiijjii"] = createExportWrapper("dynCall_viiiifiijjii");
+
+var dynCall_vijiiifiijii = Module["dynCall_vijiiifiijii"] = createExportWrapper("dynCall_vijiiifiijii");
+
+var dynCall_vijiiifiiijjii = Module["dynCall_vijiiifiiijjii"] = createExportWrapper("dynCall_vijiiifiiijjii");
+
+var dynCall_vijiiifiiijii = Module["dynCall_vijiiifiiijii"] = createExportWrapper("dynCall_vijiiifiiijii");
+
+var dynCall_vijiiifiiiijjii = Module["dynCall_vijiiifiiiijjii"] = createExportWrapper("dynCall_vijiiifiiiijjii");
+
+var dynCall_fijiiii = Module["dynCall_fijiiii"] = createExportWrapper("dynCall_fijiiii");
+
+var dynCall_fijiiiii = Module["dynCall_fijiiiii"] = createExportWrapper("dynCall_fijiiiii");
+
+var dynCall_iijii = Module["dynCall_iijii"] = createExportWrapper("dynCall_iijii");
+
+var dynCall_iijiijiiiii = Module["dynCall_iijiijiiiii"] = createExportWrapper("dynCall_iijiijiiiii");
+
+var dynCall_iijijiiiii = Module["dynCall_iijijiiiii"] = createExportWrapper("dynCall_iijijiiiii");
+
+var dynCall_vijijj = Module["dynCall_vijijj"] = createExportWrapper("dynCall_vijijj");
+
+var dynCall_vijiiijj = Module["dynCall_vijiiijj"] = createExportWrapper("dynCall_vijiiijj");
+
+var dynCall_vijiijj = Module["dynCall_vijiijj"] = createExportWrapper("dynCall_vijiijj");
+
+var dynCall_vijjiji = Module["dynCall_vijjiji"] = createExportWrapper("dynCall_vijjiji");
+
+var dynCall_vijjiijii = Module["dynCall_vijjiijii"] = createExportWrapper("dynCall_vijjiijii");
+
+var dynCall_fijii = Module["dynCall_fijii"] = createExportWrapper("dynCall_fijii");
+
+var dynCall_iiiiiiij = Module["dynCall_iiiiiiij"] = createExportWrapper("dynCall_iiiiiiij");
+
+var dynCall_vijiiiij = Module["dynCall_vijiiiij"] = createExportWrapper("dynCall_vijiiiij");
+
+var dynCall_jijj = Module["dynCall_jijj"] = createExportWrapper("dynCall_jijj");
+
+var dynCall_jiiif = Module["dynCall_jiiif"] = createExportWrapper("dynCall_jiiif");
+
+var dynCall_vijfff = Module["dynCall_vijfff"] = createExportWrapper("dynCall_vijfff");
+
+var dynCall_vijfiff = Module["dynCall_vijfiff"] = createExportWrapper("dynCall_vijfiff");
+
+var dynCall_vijfi = Module["dynCall_vijfi"] = createExportWrapper("dynCall_vijfi");
+
+var dynCall_vijffffi = Module["dynCall_vijffffi"] = createExportWrapper("dynCall_vijffffi");
+
+var dynCall_vijiiffi = Module["dynCall_vijiiffi"] = createExportWrapper("dynCall_vijiiffi");
+
+var dynCall_vijiifffffff = Module["dynCall_vijiifffffff"] = createExportWrapper("dynCall_vijiifffffff");
+
+var dynCall_vijifiifffffifff = Module["dynCall_vijifiifffffifff"] = createExportWrapper("dynCall_vijifiifffffifff");
+
+var dynCall_vijiiffffiffffj = Module["dynCall_vijiiffffiffffj"] = createExportWrapper("dynCall_vijiiffffiffffj");
+
+var dynCall_vijiifff = Module["dynCall_vijiifff"] = createExportWrapper("dynCall_vijiifff");
+
+var dynCall_vijiffffffff = Module["dynCall_vijiffffffff"] = createExportWrapper("dynCall_vijiffffffff");
+
+var dynCall_vijiifiififff = Module["dynCall_vijiifiififff"] = createExportWrapper("dynCall_vijiifiififff");
+
+var dynCall_vijifffij = Module["dynCall_vijifffij"] = createExportWrapper("dynCall_vijifffij");
+
+var dynCall_viijjjiifjii = Module["dynCall_viijjjiifjii"] = createExportWrapper("dynCall_viijjjiifjii");
+
+var dynCall_vijjjii = Module["dynCall_vijjjii"] = createExportWrapper("dynCall_vijjjii");
+
+var dynCall_fijj = Module["dynCall_fijj"] = createExportWrapper("dynCall_fijj");
+
+var dynCall_iijjiii = Module["dynCall_iijjiii"] = createExportWrapper("dynCall_iijjiii");
+
+var dynCall_iiiiij = Module["dynCall_iiiiij"] = createExportWrapper("dynCall_iiiiij");
+
+var dynCall_iiiiijj = Module["dynCall_iiiiijj"] = createExportWrapper("dynCall_iiiiijj");
+
+var dynCall_iiiiiijj = Module["dynCall_iiiiiijj"] = createExportWrapper("dynCall_iiiiiijj");
+
+var __emscripten_allow_main_runtime_queued_calls = Module["__emscripten_allow_main_runtime_queued_calls"] = 3352356;
+
+function invoke_vii(index, a1, a2) {
+ var sp = stackSave();
+ try {
+ getWasmTableEntry(index)(a1, a2);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_vi(index, a1) {
+ var sp = stackSave();
+ try {
+ getWasmTableEntry(index)(a1);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_viii(index, a1, a2, a3) {
+ var sp = stackSave();
+ try {
+ getWasmTableEntry(index)(a1, a2, a3);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_viiii(index, a1, a2, a3, a4) {
+ var sp = stackSave();
+ try {
+ getWasmTableEntry(index)(a1, a2, a3, a4);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_iii(index, a1, a2) {
+ var sp = stackSave();
+ try {
+ return getWasmTableEntry(index)(a1, a2);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_ii(index, a1) {
+ var sp = stackSave();
+ try {
+ return getWasmTableEntry(index)(a1);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_iiii(index, a1, a2, a3) {
+ var sp = stackSave();
+ try {
+ return getWasmTableEntry(index)(a1, a2, a3);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_iiiii(index, a1, a2, a3, a4) {
+ var sp = stackSave();
+ try {
+ return getWasmTableEntry(index)(a1, a2, a3, a4);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_viiiiiii(index, a1, a2, a3, a4, a5, a6, a7) {
+ var sp = stackSave();
+ try {
+ getWasmTableEntry(index)(a1, a2, a3, a4, a5, a6, a7);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+function invoke_iiiiiii(index, a1, a2, a3, a4, a5, a6) {
+ var sp = stackSave();
+ try {
+ return getWasmTableEntry(index)(a1, a2, a3, a4, a5, a6);
+ } catch (e) {
+ stackRestore(sp);
+ if (e !== e + 0 && e !== "longjmp") throw e;
+ _setThrew(1, 0);
+ }
+}
+
+if (!Object.getOwnPropertyDescriptor(Module, "intArrayFromString")) Module["intArrayFromString"] = (() => abort("'intArrayFromString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "intArrayToString")) Module["intArrayToString"] = (() => abort("'intArrayToString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "ccall")) Module["ccall"] = (() => abort("'ccall' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+Module["cwrap"] = cwrap;
+
+if (!Object.getOwnPropertyDescriptor(Module, "setValue")) Module["setValue"] = (() => abort("'setValue' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getValue")) Module["getValue"] = (() => abort("'getValue' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "allocate")) Module["allocate"] = (() => abort("'allocate' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "UTF8ArrayToString")) Module["UTF8ArrayToString"] = (() => abort("'UTF8ArrayToString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "UTF8ToString")) Module["UTF8ToString"] = (() => abort("'UTF8ToString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stringToUTF8Array")) Module["stringToUTF8Array"] = (() => abort("'stringToUTF8Array' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stringToUTF8")) Module["stringToUTF8"] = (() => abort("'stringToUTF8' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "lengthBytesUTF8")) Module["lengthBytesUTF8"] = (() => abort("'lengthBytesUTF8' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stackTrace")) Module["stackTrace"] = (() => abort("'stackTrace' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "addOnPreRun")) Module["addOnPreRun"] = (() => abort("'addOnPreRun' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "addOnInit")) Module["addOnInit"] = (() => abort("'addOnInit' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "addOnPreMain")) Module["addOnPreMain"] = (() => abort("'addOnPreMain' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "addOnExit")) Module["addOnExit"] = (() => abort("'addOnExit' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "addOnPostRun")) Module["addOnPostRun"] = (() => abort("'addOnPostRun' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeStringToMemory")) Module["writeStringToMemory"] = (() => abort("'writeStringToMemory' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeArrayToMemory")) Module["writeArrayToMemory"] = (() => abort("'writeArrayToMemory' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeAsciiToMemory")) Module["writeAsciiToMemory"] = (() => abort("'writeAsciiToMemory' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "addRunDependency")) Module["addRunDependency"] = (() => abort("'addRunDependency' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "removeRunDependency")) Module["removeRunDependency"] = (() => abort("'removeRunDependency' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS_createFolder")) Module["FS_createFolder"] = (() => abort("'FS_createFolder' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS_createPath")) Module["FS_createPath"] = (() => abort("'FS_createPath' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS_createDataFile")) Module["FS_createDataFile"] = (() => abort("'FS_createDataFile' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS_createPreloadedFile")) Module["FS_createPreloadedFile"] = (() => abort("'FS_createPreloadedFile' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS_createLazyFile")) Module["FS_createLazyFile"] = (() => abort("'FS_createLazyFile' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS_createLink")) Module["FS_createLink"] = (() => abort("'FS_createLink' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS_createDevice")) Module["FS_createDevice"] = (() => abort("'FS_createDevice' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS_unlink")) Module["FS_unlink"] = (() => abort("'FS_unlink' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getLEB")) Module["getLEB"] = (() => abort("'getLEB' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getFunctionTables")) Module["getFunctionTables"] = (() => abort("'getFunctionTables' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "alignFunctionTables")) Module["alignFunctionTables"] = (() => abort("'alignFunctionTables' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerFunctions")) Module["registerFunctions"] = (() => abort("'registerFunctions' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "addFunction")) Module["addFunction"] = (() => abort("'addFunction' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "removeFunction")) Module["removeFunction"] = (() => abort("'removeFunction' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getFuncWrapper")) Module["getFuncWrapper"] = (() => abort("'getFuncWrapper' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "prettyPrint")) Module["prettyPrint"] = (() => abort("'prettyPrint' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "dynCall")) Module["dynCall"] = (() => abort("'dynCall' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getCompilerSetting")) Module["getCompilerSetting"] = (() => abort("'getCompilerSetting' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "print")) Module["print"] = (() => abort("'print' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "printErr")) Module["printErr"] = (() => abort("'printErr' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getTempRet0")) Module["getTempRet0"] = (() => abort("'getTempRet0' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "setTempRet0")) Module["setTempRet0"] = (() => abort("'setTempRet0' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+Module["callMain"] = callMain;
+
+if (!Object.getOwnPropertyDescriptor(Module, "abort")) Module["abort"] = (() => abort("'abort' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+Module["keepRuntimeAlive"] = keepRuntimeAlive;
+
+if (!Object.getOwnPropertyDescriptor(Module, "zeroMemory")) Module["zeroMemory"] = (() => abort("'zeroMemory' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stringToNewUTF8")) Module["stringToNewUTF8"] = (() => abort("'stringToNewUTF8' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "emscripten_realloc_buffer")) Module["emscripten_realloc_buffer"] = (() => abort("'emscripten_realloc_buffer' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "ENV")) Module["ENV"] = (() => abort("'ENV' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "withStackSave")) Module["withStackSave"] = (() => abort("'withStackSave' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "ERRNO_CODES")) Module["ERRNO_CODES"] = (() => abort("'ERRNO_CODES' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "ERRNO_MESSAGES")) Module["ERRNO_MESSAGES"] = (() => abort("'ERRNO_MESSAGES' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "setErrNo")) Module["setErrNo"] = (() => abort("'setErrNo' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "inetPton4")) Module["inetPton4"] = (() => abort("'inetPton4' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "inetNtop4")) Module["inetNtop4"] = (() => abort("'inetNtop4' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "inetPton6")) Module["inetPton6"] = (() => abort("'inetPton6' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "inetNtop6")) Module["inetNtop6"] = (() => abort("'inetNtop6' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "readSockaddr")) Module["readSockaddr"] = (() => abort("'readSockaddr' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeSockaddr")) Module["writeSockaddr"] = (() => abort("'writeSockaddr' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "DNS")) Module["DNS"] = (() => abort("'DNS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getHostByName")) Module["getHostByName"] = (() => abort("'getHostByName' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "Protocols")) Module["Protocols"] = (() => abort("'Protocols' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "Sockets")) Module["Sockets"] = (() => abort("'Sockets' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getRandomDevice")) Module["getRandomDevice"] = (() => abort("'getRandomDevice' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "traverseStack")) Module["traverseStack"] = (() => abort("'traverseStack' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "convertFrameToPC")) Module["convertFrameToPC"] = (() => abort("'convertFrameToPC' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "UNWIND_CACHE")) Module["UNWIND_CACHE"] = (() => abort("'UNWIND_CACHE' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "saveInUnwindCache")) Module["saveInUnwindCache"] = (() => abort("'saveInUnwindCache' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "convertPCtoSourceLocation")) Module["convertPCtoSourceLocation"] = (() => abort("'convertPCtoSourceLocation' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "readAsmConstArgsArray")) Module["readAsmConstArgsArray"] = (() => abort("'readAsmConstArgsArray' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "readAsmConstArgs")) Module["readAsmConstArgs"] = (() => abort("'readAsmConstArgs' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "mainThreadEM_ASM")) Module["mainThreadEM_ASM"] = (() => abort("'mainThreadEM_ASM' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "jstoi_q")) Module["jstoi_q"] = (() => abort("'jstoi_q' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "jstoi_s")) Module["jstoi_s"] = (() => abort("'jstoi_s' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getExecutableName")) Module["getExecutableName"] = (() => abort("'getExecutableName' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "listenOnce")) Module["listenOnce"] = (() => abort("'listenOnce' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "autoResumeAudioContext")) Module["autoResumeAudioContext"] = (() => abort("'autoResumeAudioContext' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "dynCallLegacy")) Module["dynCallLegacy"] = (() => abort("'dynCallLegacy' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getDynCaller")) Module["getDynCaller"] = (() => abort("'getDynCaller' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "dynCall")) Module["dynCall"] = (() => abort("'dynCall' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "callRuntimeCallbacks")) Module["callRuntimeCallbacks"] = (() => abort("'callRuntimeCallbacks' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "wasmTableMirror")) Module["wasmTableMirror"] = (() => abort("'wasmTableMirror' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "setWasmTableEntry")) Module["setWasmTableEntry"] = (() => abort("'setWasmTableEntry' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getWasmTableEntry")) Module["getWasmTableEntry"] = (() => abort("'getWasmTableEntry' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "handleException")) Module["handleException"] = (() => abort("'handleException' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "runtimeKeepalivePush")) Module["runtimeKeepalivePush"] = (() => abort("'runtimeKeepalivePush' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "runtimeKeepalivePop")) Module["runtimeKeepalivePop"] = (() => abort("'runtimeKeepalivePop' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "callUserCallback")) Module["callUserCallback"] = (() => abort("'callUserCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "maybeExit")) Module["maybeExit"] = (() => abort("'maybeExit' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "safeSetTimeout")) Module["safeSetTimeout"] = (() => abort("'safeSetTimeout' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "asmjsMangle")) Module["asmjsMangle"] = (() => abort("'asmjsMangle' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "asyncLoad")) Module["asyncLoad"] = (() => abort("'asyncLoad' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "alignMemory")) Module["alignMemory"] = (() => abort("'alignMemory' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "mmapAlloc")) Module["mmapAlloc"] = (() => abort("'mmapAlloc' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "reallyNegative")) Module["reallyNegative"] = (() => abort("'reallyNegative' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "unSign")) Module["unSign"] = (() => abort("'unSign' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "reSign")) Module["reSign"] = (() => abort("'reSign' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "formatString")) Module["formatString"] = (() => abort("'formatString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "PATH")) Module["PATH"] = (() => abort("'PATH' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "PATH_FS")) Module["PATH_FS"] = (() => abort("'PATH_FS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "SYSCALLS")) Module["SYSCALLS"] = (() => abort("'SYSCALLS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getSocketFromFD")) Module["getSocketFromFD"] = (() => abort("'getSocketFromFD' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getSocketAddress")) Module["getSocketAddress"] = (() => abort("'getSocketAddress' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "JSEvents")) Module["JSEvents"] = (() => abort("'JSEvents' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerKeyEventCallback")) Module["registerKeyEventCallback"] = (() => abort("'registerKeyEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "specialHTMLTargets")) Module["specialHTMLTargets"] = (() => abort("'specialHTMLTargets' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "maybeCStringToJsString")) Module["maybeCStringToJsString"] = (() => abort("'maybeCStringToJsString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "findEventTarget")) Module["findEventTarget"] = (() => abort("'findEventTarget' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "findCanvasEventTarget")) Module["findCanvasEventTarget"] = (() => abort("'findCanvasEventTarget' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getBoundingClientRect")) Module["getBoundingClientRect"] = (() => abort("'getBoundingClientRect' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillMouseEventData")) Module["fillMouseEventData"] = (() => abort("'fillMouseEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerMouseEventCallback")) Module["registerMouseEventCallback"] = (() => abort("'registerMouseEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerWheelEventCallback")) Module["registerWheelEventCallback"] = (() => abort("'registerWheelEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerUiEventCallback")) Module["registerUiEventCallback"] = (() => abort("'registerUiEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerFocusEventCallback")) Module["registerFocusEventCallback"] = (() => abort("'registerFocusEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillDeviceOrientationEventData")) Module["fillDeviceOrientationEventData"] = (() => abort("'fillDeviceOrientationEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerDeviceOrientationEventCallback")) Module["registerDeviceOrientationEventCallback"] = (() => abort("'registerDeviceOrientationEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillDeviceMotionEventData")) Module["fillDeviceMotionEventData"] = (() => abort("'fillDeviceMotionEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerDeviceMotionEventCallback")) Module["registerDeviceMotionEventCallback"] = (() => abort("'registerDeviceMotionEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "screenOrientation")) Module["screenOrientation"] = (() => abort("'screenOrientation' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillOrientationChangeEventData")) Module["fillOrientationChangeEventData"] = (() => abort("'fillOrientationChangeEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerOrientationChangeEventCallback")) Module["registerOrientationChangeEventCallback"] = (() => abort("'registerOrientationChangeEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillFullscreenChangeEventData")) Module["fillFullscreenChangeEventData"] = (() => abort("'fillFullscreenChangeEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerFullscreenChangeEventCallback")) Module["registerFullscreenChangeEventCallback"] = (() => abort("'registerFullscreenChangeEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerRestoreOldStyle")) Module["registerRestoreOldStyle"] = (() => abort("'registerRestoreOldStyle' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "hideEverythingExceptGivenElement")) Module["hideEverythingExceptGivenElement"] = (() => abort("'hideEverythingExceptGivenElement' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "restoreHiddenElements")) Module["restoreHiddenElements"] = (() => abort("'restoreHiddenElements' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "setLetterbox")) Module["setLetterbox"] = (() => abort("'setLetterbox' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "currentFullscreenStrategy")) Module["currentFullscreenStrategy"] = (() => abort("'currentFullscreenStrategy' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "restoreOldWindowedStyle")) Module["restoreOldWindowedStyle"] = (() => abort("'restoreOldWindowedStyle' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "softFullscreenResizeWebGLRenderTarget")) Module["softFullscreenResizeWebGLRenderTarget"] = (() => abort("'softFullscreenResizeWebGLRenderTarget' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "doRequestFullscreen")) Module["doRequestFullscreen"] = (() => abort("'doRequestFullscreen' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillPointerlockChangeEventData")) Module["fillPointerlockChangeEventData"] = (() => abort("'fillPointerlockChangeEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerPointerlockChangeEventCallback")) Module["registerPointerlockChangeEventCallback"] = (() => abort("'registerPointerlockChangeEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerPointerlockErrorEventCallback")) Module["registerPointerlockErrorEventCallback"] = (() => abort("'registerPointerlockErrorEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "requestPointerLock")) Module["requestPointerLock"] = (() => abort("'requestPointerLock' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillVisibilityChangeEventData")) Module["fillVisibilityChangeEventData"] = (() => abort("'fillVisibilityChangeEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerVisibilityChangeEventCallback")) Module["registerVisibilityChangeEventCallback"] = (() => abort("'registerVisibilityChangeEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerTouchEventCallback")) Module["registerTouchEventCallback"] = (() => abort("'registerTouchEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillGamepadEventData")) Module["fillGamepadEventData"] = (() => abort("'fillGamepadEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerGamepadEventCallback")) Module["registerGamepadEventCallback"] = (() => abort("'registerGamepadEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerBeforeUnloadEventCallback")) Module["registerBeforeUnloadEventCallback"] = (() => abort("'registerBeforeUnloadEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "fillBatteryEventData")) Module["fillBatteryEventData"] = (() => abort("'fillBatteryEventData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "battery")) Module["battery"] = (() => abort("'battery' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerBatteryEventCallback")) Module["registerBatteryEventCallback"] = (() => abort("'registerBatteryEventCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "setCanvasElementSize")) Module["setCanvasElementSize"] = (() => abort("'setCanvasElementSize' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getCanvasElementSize")) Module["getCanvasElementSize"] = (() => abort("'getCanvasElementSize' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "demangle")) Module["demangle"] = (() => abort("'demangle' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "demangleAll")) Module["demangleAll"] = (() => abort("'demangleAll' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "jsStackTrace")) Module["jsStackTrace"] = (() => abort("'jsStackTrace' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stackTrace")) Module["stackTrace"] = (() => abort("'stackTrace' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getEnvStrings")) Module["getEnvStrings"] = (() => abort("'getEnvStrings' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "checkWasiClock")) Module["checkWasiClock"] = (() => abort("'checkWasiClock' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeI53ToI64")) Module["writeI53ToI64"] = (() => abort("'writeI53ToI64' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeI53ToI64Clamped")) Module["writeI53ToI64Clamped"] = (() => abort("'writeI53ToI64Clamped' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeI53ToI64Signaling")) Module["writeI53ToI64Signaling"] = (() => abort("'writeI53ToI64Signaling' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeI53ToU64Clamped")) Module["writeI53ToU64Clamped"] = (() => abort("'writeI53ToU64Clamped' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeI53ToU64Signaling")) Module["writeI53ToU64Signaling"] = (() => abort("'writeI53ToU64Signaling' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "readI53FromI64")) Module["readI53FromI64"] = (() => abort("'readI53FromI64' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "readI53FromU64")) Module["readI53FromU64"] = (() => abort("'readI53FromU64' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "convertI32PairToI53")) Module["convertI32PairToI53"] = (() => abort("'convertI32PairToI53' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "convertU32PairToI53")) Module["convertU32PairToI53"] = (() => abort("'convertU32PairToI53' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "setImmediateWrapped")) Module["setImmediateWrapped"] = (() => abort("'setImmediateWrapped' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "clearImmediateWrapped")) Module["clearImmediateWrapped"] = (() => abort("'clearImmediateWrapped' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "polyfillSetImmediate")) Module["polyfillSetImmediate"] = (() => abort("'polyfillSetImmediate' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "uncaughtExceptionCount")) Module["uncaughtExceptionCount"] = (() => abort("'uncaughtExceptionCount' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "exceptionLast")) Module["exceptionLast"] = (() => abort("'exceptionLast' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "exceptionCaught")) Module["exceptionCaught"] = (() => abort("'exceptionCaught' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "ExceptionInfo")) Module["ExceptionInfo"] = (() => abort("'ExceptionInfo' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "CatchInfo")) Module["CatchInfo"] = (() => abort("'CatchInfo' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "exception_addRef")) Module["exception_addRef"] = (() => abort("'exception_addRef' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "exception_decRef")) Module["exception_decRef"] = (() => abort("'exception_decRef' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "Browser")) Module["Browser"] = (() => abort("'Browser' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "funcWrappers")) Module["funcWrappers"] = (() => abort("'funcWrappers' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "getFuncWrapper")) Module["getFuncWrapper"] = (() => abort("'getFuncWrapper' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "setMainLoop")) Module["setMainLoop"] = (() => abort("'setMainLoop' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "wget")) Module["wget"] = (() => abort("'wget' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "tempFixedLengthArray")) Module["tempFixedLengthArray"] = (() => abort("'tempFixedLengthArray' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "miniTempWebGLFloatBuffers")) Module["miniTempWebGLFloatBuffers"] = (() => abort("'miniTempWebGLFloatBuffers' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "heapObjectForWebGLType")) Module["heapObjectForWebGLType"] = (() => abort("'heapObjectForWebGLType' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "heapAccessShiftForWebGLHeap")) Module["heapAccessShiftForWebGLHeap"] = (() => abort("'heapAccessShiftForWebGLHeap' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GL")) Module["GL"] = (() => abort("'GL' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "emscriptenWebGLGet")) Module["emscriptenWebGLGet"] = (() => abort("'emscriptenWebGLGet' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "computeUnpackAlignedImageSize")) Module["computeUnpackAlignedImageSize"] = (() => abort("'computeUnpackAlignedImageSize' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "emscriptenWebGLGetTexPixelData")) Module["emscriptenWebGLGetTexPixelData"] = (() => abort("'emscriptenWebGLGetTexPixelData' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "emscriptenWebGLGetUniform")) Module["emscriptenWebGLGetUniform"] = (() => abort("'emscriptenWebGLGetUniform' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "webglGetUniformLocation")) Module["webglGetUniformLocation"] = (() => abort("'webglGetUniformLocation' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "webglPrepareUniformLocationsBeforeFirstUse")) Module["webglPrepareUniformLocationsBeforeFirstUse"] = (() => abort("'webglPrepareUniformLocationsBeforeFirstUse' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "webglGetLeftBracePos")) Module["webglGetLeftBracePos"] = (() => abort("'webglGetLeftBracePos' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "emscriptenWebGLGetVertexAttrib")) Module["emscriptenWebGLGetVertexAttrib"] = (() => abort("'emscriptenWebGLGetVertexAttrib' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "writeGLArray")) Module["writeGLArray"] = (() => abort("'writeGLArray' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "FS")) Module["FS"] = (() => abort("'FS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "MEMFS")) Module["MEMFS"] = (() => abort("'MEMFS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "TTY")) Module["TTY"] = (() => abort("'TTY' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "PIPEFS")) Module["PIPEFS"] = (() => abort("'PIPEFS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "SOCKFS")) Module["SOCKFS"] = (() => abort("'SOCKFS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "_setNetworkCallback")) Module["_setNetworkCallback"] = (() => abort("'_setNetworkCallback' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "AL")) Module["AL"] = (() => abort("'AL' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "SDL_unicode")) Module["SDL_unicode"] = (() => abort("'SDL_unicode' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "SDL_ttfContext")) Module["SDL_ttfContext"] = (() => abort("'SDL_ttfContext' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "SDL_audio")) Module["SDL_audio"] = (() => abort("'SDL_audio' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "SDL")) Module["SDL"] = (() => abort("'SDL' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "SDL_gfx")) Module["SDL_gfx"] = (() => abort("'SDL_gfx' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GLUT")) Module["GLUT"] = (() => abort("'GLUT' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "EGL")) Module["EGL"] = (() => abort("'EGL' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GLFW_Window")) Module["GLFW_Window"] = (() => abort("'GLFW_Window' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GLFW")) Module["GLFW"] = (() => abort("'GLFW' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GLEW")) Module["GLEW"] = (() => abort("'GLEW' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "IDBStore")) Module["IDBStore"] = (() => abort("'IDBStore' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "runAndAbortIfError")) Module["runAndAbortIfError"] = (() => abort("'runAndAbortIfError' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "emscriptenWebGLGetIndexed")) Module["emscriptenWebGLGetIndexed"] = (() => abort("'emscriptenWebGLGetIndexed' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+Module["PThread"] = PThread;
+
+if (!Object.getOwnPropertyDescriptor(Module, "ptrToString")) Module["ptrToString"] = (() => abort("'ptrToString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "killThread")) Module["killThread"] = (() => abort("'killThread' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "cleanupThread")) Module["cleanupThread"] = (() => abort("'cleanupThread' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "registerTlsInit")) Module["registerTlsInit"] = (() => abort("'registerTlsInit' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "cancelThread")) Module["cancelThread"] = (() => abort("'cancelThread' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "spawnThread")) Module["spawnThread"] = (() => abort("'spawnThread' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "exitOnMainThread")) Module["exitOnMainThread"] = (() => abort("'exitOnMainThread' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "establishStackSpace")) Module["establishStackSpace"] = (() => abort("'establishStackSpace' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "invokeEntryPoint")) Module["invokeEntryPoint"] = (() => abort("'invokeEntryPoint' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotWebSocket")) Module["GodotWebSocket"] = (() => abort("'GodotWebSocket' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotWebSocket__user")) Module["GodotWebSocket__user"] = (() => abort("'GodotWebSocket__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotRTCDataChannel")) Module["GodotRTCDataChannel"] = (() => abort("'GodotRTCDataChannel' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotRTCDataChannel__user")) Module["GodotRTCDataChannel__user"] = (() => abort("'GodotRTCDataChannel__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotRTCPeerConnection")) Module["GodotRTCPeerConnection"] = (() => abort("'GodotRTCPeerConnection' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotRTCPeerConnection__user")) Module["GodotRTCPeerConnection__user"] = (() => abort("'GodotRTCPeerConnection__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotAudio")) Module["GodotAudio"] = (() => abort("'GodotAudio' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotAudio__user")) Module["GodotAudio__user"] = (() => abort("'GodotAudio__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotAudioWorklet")) Module["GodotAudioWorklet"] = (() => abort("'GodotAudioWorklet' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotAudioWorklet__user")) Module["GodotAudioWorklet__user"] = (() => abort("'GodotAudioWorklet__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotAudioScript")) Module["GodotAudioScript"] = (() => abort("'GodotAudioScript' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotAudioScript__user")) Module["GodotAudioScript__user"] = (() => abort("'GodotAudioScript__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotDisplayVK")) Module["GodotDisplayVK"] = (() => abort("'GodotDisplayVK' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotDisplayVK__user")) Module["GodotDisplayVK__user"] = (() => abort("'GodotDisplayVK__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotDisplayCursor")) Module["GodotDisplayCursor"] = (() => abort("'GodotDisplayCursor' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotDisplayCursor__user")) Module["GodotDisplayCursor__user"] = (() => abort("'GodotDisplayCursor__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotDisplayScreen")) Module["GodotDisplayScreen"] = (() => abort("'GodotDisplayScreen' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotDisplayScreen__user")) Module["GodotDisplayScreen__user"] = (() => abort("'GodotDisplayScreen__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotDisplay")) Module["GodotDisplay"] = (() => abort("'GodotDisplay' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotDisplay__user")) Module["GodotDisplay__user"] = (() => abort("'GodotDisplay__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotFetch")) Module["GodotFetch"] = (() => abort("'GodotFetch' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotFetch__user")) Module["GodotFetch__user"] = (() => abort("'GodotFetch__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "IDHandler")) Module["IDHandler"] = (() => abort("'IDHandler' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "IDHandler__user")) Module["IDHandler__user"] = (() => abort("'IDHandler__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotConfig")) Module["GodotConfig"] = (() => abort("'GodotConfig' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotConfig__user")) Module["GodotConfig__user"] = (() => abort("'GodotConfig__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotFS")) Module["GodotFS"] = (() => abort("'GodotFS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotFS__user")) Module["GodotFS__user"] = (() => abort("'GodotFS__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotOS")) Module["GodotOS"] = (() => abort("'GodotOS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotOS__user")) Module["GodotOS__user"] = (() => abort("'GodotOS__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotEventListeners")) Module["GodotEventListeners"] = (() => abort("'GodotEventListeners' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotEventListeners__user")) Module["GodotEventListeners__user"] = (() => abort("'GodotEventListeners__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotPWA")) Module["GodotPWA"] = (() => abort("'GodotPWA' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotPWA__user")) Module["GodotPWA__user"] = (() => abort("'GodotPWA__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "IDBFS")) Module["IDBFS"] = (() => abort("'IDBFS' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotRuntime")) Module["GodotRuntime"] = (() => abort("'GodotRuntime' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotRuntime__user")) Module["GodotRuntime__user"] = (() => abort("'GodotRuntime__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotInputGamepads")) Module["GodotInputGamepads"] = (() => abort("'GodotInputGamepads' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotInputGamepads__user")) Module["GodotInputGamepads__user"] = (() => abort("'GodotInputGamepads__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotInputDragDrop")) Module["GodotInputDragDrop"] = (() => abort("'GodotInputDragDrop' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotInputDragDrop__user")) Module["GodotInputDragDrop__user"] = (() => abort("'GodotInputDragDrop__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotInput")) Module["GodotInput"] = (() => abort("'GodotInput' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotInput__user")) Module["GodotInput__user"] = (() => abort("'GodotInput__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotWebGL2")) Module["GodotWebGL2"] = (() => abort("'GodotWebGL2' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "GodotWebGL2__user")) Module["GodotWebGL2__user"] = (() => abort("'GodotWebGL2__user' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "warnOnce")) Module["warnOnce"] = (() => abort("'warnOnce' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stackSave")) Module["stackSave"] = (() => abort("'stackSave' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stackRestore")) Module["stackRestore"] = (() => abort("'stackRestore' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stackAlloc")) Module["stackAlloc"] = (() => abort("'stackAlloc' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "AsciiToString")) Module["AsciiToString"] = (() => abort("'AsciiToString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stringToAscii")) Module["stringToAscii"] = (() => abort("'stringToAscii' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "UTF16ToString")) Module["UTF16ToString"] = (() => abort("'UTF16ToString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stringToUTF16")) Module["stringToUTF16"] = (() => abort("'stringToUTF16' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "lengthBytesUTF16")) Module["lengthBytesUTF16"] = (() => abort("'lengthBytesUTF16' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "UTF32ToString")) Module["UTF32ToString"] = (() => abort("'UTF32ToString' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "stringToUTF32")) Module["stringToUTF32"] = (() => abort("'stringToUTF32' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "lengthBytesUTF32")) Module["lengthBytesUTF32"] = (() => abort("'lengthBytesUTF32' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "allocateUTF8")) Module["allocateUTF8"] = (() => abort("'allocateUTF8' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+if (!Object.getOwnPropertyDescriptor(Module, "allocateUTF8OnStack")) Module["allocateUTF8OnStack"] = (() => abort("'allocateUTF8OnStack' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"));
+
+Module["writeStackCookie"] = writeStackCookie;
+
+Module["checkStackCookie"] = checkStackCookie;
+
+Module["PThread"] = PThread;
+
+Module["wasmMemory"] = wasmMemory;
+
+Module["ExitStatus"] = ExitStatus;
+
+if (!Object.getOwnPropertyDescriptor(Module, "ALLOC_NORMAL")) Object.defineProperty(Module, "ALLOC_NORMAL", {
+ configurable: true,
+ get: function() {
+ abort("'ALLOC_NORMAL' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)");
+ }
+});
+
+if (!Object.getOwnPropertyDescriptor(Module, "ALLOC_STACK")) Object.defineProperty(Module, "ALLOC_STACK", {
+ configurable: true,
+ get: function() {
+ abort("'ALLOC_STACK' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)");
+ }
+});
+
+var calledRun;
+
+function ExitStatus(status) {
+ this.name = "ExitStatus";
+ this.message = "Program terminated with exit(" + status + ")";
+ this.status = status;
+}
+
+var calledMain = false;
+
+dependenciesFulfilled = function runCaller() {
+ if (!calledRun) run();
+ if (!calledRun) dependenciesFulfilled = runCaller;
+};
+
+function callMain(args) {
+ assert(runDependencies == 0, 'cannot call main when async dependencies remain! (listen on Module["onRuntimeInitialized"])');
+ assert(__ATPRERUN__.length == 0, "cannot call main when preRun functions remain to be called");
+ var entryFunction = Module["_main"];
+ args = args || [];
+ var argc = args.length + 1;
+ var argv = stackAlloc((argc + 1) * 4);
+ GROWABLE_HEAP_I32()[argv >> 2] = allocateUTF8OnStack(thisProgram);
+ for (var i = 1; i < argc; i++) {
+ GROWABLE_HEAP_I32()[(argv >> 2) + i] = allocateUTF8OnStack(args[i - 1]);
+ }
+ GROWABLE_HEAP_I32()[(argv >> 2) + argc] = 0;
+ try {
+ var ret = entryFunction(argc, argv);
+ exit(ret, true);
+ return ret;
+ } catch (e) {
+ return handleException(e);
+ } finally {
+ calledMain = true;
+ }
+}
+
+function stackCheckInit() {
+ _emscripten_stack_init();
+ writeStackCookie();
+}
+
+function run(args) {
+ args = args || arguments_;
+ if (runDependencies > 0) {
+ return;
+ }
+ stackCheckInit();
+ if (ENVIRONMENT_IS_PTHREAD) {
+ readyPromiseResolve(Module);
+ initRuntime();
+ postMessage({
+ "cmd": "loaded"
+ });
+ return;
+ }
+ preRun();
+ if (runDependencies > 0) {
+ return;
+ }
+ function doRun() {
+ if (calledRun) return;
+ calledRun = true;
+ Module["calledRun"] = true;
+ if (ABORT) return;
+ initRuntime();
+ preMain();
+ readyPromiseResolve(Module);
+ if (Module["onRuntimeInitialized"]) Module["onRuntimeInitialized"]();
+ if (shouldRunNow) callMain(args);
+ postRun();
+ }
+ if (Module["setStatus"]) {
+ Module["setStatus"]("Running...");
+ setTimeout(function() {
+ setTimeout(function() {
+ Module["setStatus"]("");
+ }, 1);
+ doRun();
+ }, 1);
+ } else {
+ doRun();
+ }
+ checkStackCookie();
+}
+
+Module["run"] = run;
+
+function exit(status, implicit) {
+ EXITSTATUS = status;
+ if (!implicit) {
+ if (ENVIRONMENT_IS_PTHREAD) {
+ exitOnMainThread(status);
+ throw "unwind";
+ } else {}
+ }
+ if (keepRuntimeAlive()) {
+ if (!implicit) {
+ var msg = "program exited (with status: " + status + "), but keepRuntimeAlive() is set (counter=" + runtimeKeepaliveCounter + ") due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)";
+ readyPromiseReject(msg);
+ err(msg);
+ }
+ } else {
+ exitRuntime();
+ }
+ procExit(status);
+}
+
+function procExit(code) {
+ EXITSTATUS = code;
+ if (!keepRuntimeAlive()) {
+ PThread.terminateAllThreads();
+ if (Module["onExit"]) Module["onExit"](code);
+ ABORT = true;
+ }
+ quit_(code, new ExitStatus(code));
+}
+
+if (Module["preInit"]) {
+ if (typeof Module["preInit"] == "function") Module["preInit"] = [ Module["preInit"] ];
+ while (Module["preInit"].length > 0) {
+ Module["preInit"].pop()();
+ }
+}
+
+var shouldRunNow = false;
+
+if (Module["noInitialRun"]) shouldRunNow = false;
+
+run();
+
+
+ return Godot.ready
+}
+);
+})();
+if (typeof exports === 'object' && typeof module === 'object')
+ module.exports = Godot;
+else if (typeof define === 'function' && define['amd'])
+ define([], function() { return Godot; });
+else if (typeof exports === 'object')
+ exports["Godot"] = Godot;
+
+const Features = { // eslint-disable-line no-unused-vars
+ /**
+ * Check whether WebGL is available. Optionally, specify a particular version of WebGL to check for.
+ *
+ * @param {number=} [majorVersion=1] The major WebGL version to check for.
+ * @returns {boolean} If the given major version of WebGL is available.
+ * @function Engine.isWebGLAvailable
+ */
+ isWebGLAvailable: function (majorVersion = 1) {
+ try {
+ return !!document.createElement('canvas').getContext(['webgl', 'webgl2'][majorVersion - 1]);
+ } catch (e) { /* Not available */ }
+ return false;
+ },
+
+ /**
+ * Check whether the Fetch API available and supports streaming responses.
+ *
+ * @returns {boolean} If the Fetch API is available and supports streaming responses.
+ * @function Engine.isFetchAvailable
+ */
+ isFetchAvailable: function () {
+ return 'fetch' in window && 'Response' in window && 'body' in window.Response.prototype;
+ },
+
+ /**
+ * Check whether the engine is running in a Secure Context.
+ *
+ * @returns {boolean} If the engine is running in a Secure Context.
+ * @function Engine.isSecureContext
+ */
+ isSecureContext: function () {
+ return window['isSecureContext'] === true;
+ },
+
+ /**
+ * Check whether the engine is cross origin isolated.
+ * This value is dependent on Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers sent by the server.
+ *
+ * @returns {boolean} If the engine is running in a Secure Context.
+ * @function Engine.isSecureContext
+ */
+ isCrossOriginIsolated: function () {
+ return window['crossOriginIsolated'] === true;
+ },
+
+ /**
+ * Check whether SharedBufferArray is available.
+ *
+ * Most browsers require the page to be running in a secure context, and the
+ * the server to provide specific CORS headers for SharedArrayBuffer to be available.
+ *
+ * @returns {boolean} If SharedArrayBuffer is available.
+ * @function Engine.isSharedArrayBufferAvailable
+ */
+ isSharedArrayBufferAvailable: function () {
+ return 'SharedArrayBuffer' in window;
+ },
+
+ /**
+ * Check whether the AudioContext supports AudioWorkletNodes.
+ *
+ * @returns {boolean} If AudioWorkletNode is available.
+ * @function Engine.isAudioWorkletAvailable
+ */
+ isAudioWorkletAvailable: function () {
+ return 'AudioContext' in window && 'audioWorklet' in AudioContext.prototype;
+ },
+
+ /**
+ * Return an array of missing required features (as string).
+ *
+ * @returns {Array} A list of human-readable missing features.
+ * @function Engine.getMissingFeatures
+ */
+ getMissingFeatures: function () {
+ const missing = [];
+ if (!Features.isWebGLAvailable(2)) {
+ missing.push('WebGL2 - Check web browser configuration and hardware support');
+ }
+ if (!Features.isFetchAvailable()) {
+ missing.push('Fetch - Check web browser version');
+ }
+ if (!Features.isSecureContext()) {
+ missing.push('Secure Context - Check web server configuration (use HTTPS)');
+ }
+ if (!Features.isCrossOriginIsolated()) {
+ missing.push('Cross Origin Isolation - Check web server configuration (send correct headers)');
+ }
+ if (!Features.isSharedArrayBufferAvailable()) {
+ missing.push('SharedArrayBuffer - Check web server configuration (send correct headers)');
+ }
+ // Audio is normally optional since we have a dummy fallback.
+ return missing;
+ },
+};
+
+const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars
+ function getTrackedResponse(response, load_status) {
+ function onloadprogress(reader, controller) {
+ return reader.read().then(function (result) {
+ if (load_status.done) {
+ return Promise.resolve();
+ }
+ if (result.value) {
+ controller.enqueue(result.value);
+ load_status.loaded += result.value.length;
+ }
+ if (!result.done) {
+ return onloadprogress(reader, controller);
+ }
+ load_status.done = true;
+ return Promise.resolve();
+ });
+ }
+ const reader = response.body.getReader();
+ return new Response(new ReadableStream({
+ start: function (controller) {
+ onloadprogress(reader, controller).then(function () {
+ controller.close();
+ });
+ },
+ }), { headers: response.headers });
+ }
+
+ function loadFetch(file, tracker, fileSize, raw) {
+ tracker[file] = {
+ total: fileSize || 0,
+ loaded: 0,
+ done: false,
+ };
+ return fetch(file).then(function (response) {
+ if (!response.ok) {
+ return Promise.reject(new Error(`Failed loading file '${file}'`));
+ }
+ const tr = getTrackedResponse(response, tracker[file]);
+ if (raw) {
+ return Promise.resolve(tr);
+ }
+ return tr.arrayBuffer();
+ });
+ }
+
+ function retry(func, attempts = 1) {
+ function onerror(err) {
+ if (attempts <= 1) {
+ return Promise.reject(err);
+ }
+ return new Promise(function (resolve, reject) {
+ setTimeout(function () {
+ retry(func, attempts - 1).then(resolve).catch(reject);
+ }, 1000);
+ });
+ }
+ return func().catch(onerror);
+ }
+
+ const DOWNLOAD_ATTEMPTS_MAX = 4;
+ const loadingFiles = {};
+ const lastProgress = { loaded: 0, total: 0 };
+ let progressFunc = null;
+
+ const animateProgress = function () {
+ let loaded = 0;
+ let total = 0;
+ let totalIsValid = true;
+ let progressIsFinal = true;
+
+ Object.keys(loadingFiles).forEach(function (file) {
+ const stat = loadingFiles[file];
+ if (!stat.done) {
+ progressIsFinal = false;
+ }
+ if (!totalIsValid || stat.total === 0) {
+ totalIsValid = false;
+ total = 0;
+ } else {
+ total += stat.total;
+ }
+ loaded += stat.loaded;
+ });
+ if (loaded !== lastProgress.loaded || total !== lastProgress.total) {
+ lastProgress.loaded = loaded;
+ lastProgress.total = total;
+ if (typeof progressFunc === 'function') {
+ progressFunc(loaded, total);
+ }
+ }
+ if (!progressIsFinal) {
+ requestAnimationFrame(animateProgress);
+ }
+ };
+
+ this.animateProgress = animateProgress;
+
+ this.setProgressFunc = function (callback) {
+ progressFunc = callback;
+ };
+
+ this.loadPromise = function (file, fileSize, raw = false) {
+ return retry(loadFetch.bind(null, file, loadingFiles, fileSize, raw), DOWNLOAD_ATTEMPTS_MAX);
+ };
+
+ this.preloadedFiles = [];
+ this.preload = function (pathOrBuffer, destPath, fileSize) {
+ let buffer = null;
+ if (typeof pathOrBuffer === 'string') {
+ const me = this;
+ return this.loadPromise(pathOrBuffer, fileSize).then(function (buf) {
+ me.preloadedFiles.push({
+ path: destPath || pathOrBuffer,
+ buffer: buf,
+ });
+ return Promise.resolve();
+ });
+ } else if (pathOrBuffer instanceof ArrayBuffer) {
+ buffer = new Uint8Array(pathOrBuffer);
+ } else if (ArrayBuffer.isView(pathOrBuffer)) {
+ buffer = new Uint8Array(pathOrBuffer.buffer);
+ }
+ if (buffer) {
+ this.preloadedFiles.push({
+ path: destPath,
+ buffer: pathOrBuffer,
+ });
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error('Invalid object for preloading'));
+ };
+};
+
+/**
+ * An object used to configure the Engine instance based on godot export options, and to override those in custom HTML
+ * templates if needed.
+ *
+ * @header Engine configuration
+ * @summary The Engine configuration object. This is just a typedef, create it like a regular object, e.g.:
+ *
+ * ``const MyConfig = { executable: 'godot', unloadAfterInit: false }``
+ *
+ * @typedef {Object} EngineConfig
+ */
+const EngineConfig = {}; // eslint-disable-line no-unused-vars
+
+/**
+ * @struct
+ * @constructor
+ * @ignore
+ */
+const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-vars
+ const cfg = /** @lends {InternalConfig.prototype} */ {
+ /**
+ * Whether the unload the engine automatically after the instance is initialized.
+ *
+ * @memberof EngineConfig
+ * @default
+ * @type {boolean}
+ */
+ unloadAfterInit: true,
+ /**
+ * The HTML DOM Canvas object to use.
+ *
+ * By default, the first canvas element in the document will be used is none is specified.
+ *
+ * @memberof EngineConfig
+ * @default
+ * @type {?HTMLCanvasElement}
+ */
+ canvas: null,
+ /**
+ * The name of the WASM file without the extension. (Set by Godot Editor export process).
+ *
+ * @memberof EngineConfig
+ * @default
+ * @type {string}
+ */
+ executable: '',
+ /**
+ * An alternative name for the game pck to load. The executable name is used otherwise.
+ *
+ * @memberof EngineConfig
+ * @default
+ * @type {?string}
+ */
+ mainPack: null,
+ /**
+ * Specify a language code to select the proper localization for the game.
+ *
+ * The browser locale will be used if none is specified. See complete list of
+ * :ref:`supported locales `.
+ *
+ * @memberof EngineConfig
+ * @type {?string}
+ * @default
+ */
+ locale: null,
+ /**
+ * The canvas resize policy determines how the canvas should be resized by Godot.
+ *
+ * ``0`` means Godot won't do any resizing. This is useful if you want to control the canvas size from
+ * javascript code in your template.
+ *
+ * ``1`` means Godot will resize the canvas on start, and when changing window size via engine functions.
+ *
+ * ``2`` means Godot will adapt the canvas size to match the whole browser window.
+ *
+ * @memberof EngineConfig
+ * @type {number}
+ * @default
+ */
+ canvasResizePolicy: 2,
+ /**
+ * The arguments to be passed as command line arguments on startup.
+ *
+ * See :ref:`command line tutorial `.
+ *
+ * **Note**: :js:meth:`startGame ` will always add the ``--main-pack`` argument.
+ *
+ * @memberof EngineConfig
+ * @type {Array}
+ * @default
+ */
+ args: [],
+ /**
+ * When enabled, the game canvas will automatically grab the focus when the engine starts.
+ *
+ * @memberof EngineConfig
+ * @type {boolean}
+ * @default
+ */
+ focusCanvas: true,
+ /**
+ * When enabled, this will turn on experimental virtual keyboard support on mobile.
+ *
+ * @memberof EngineConfig
+ * @type {boolean}
+ * @default
+ */
+ experimentalVK: false,
+ /**
+ * The progressive web app service worker to install.
+ * @memberof EngineConfig
+ * @default
+ * @type {string}
+ */
+ serviceWorker: '',
+ /**
+ * @ignore
+ * @type {Array.}
+ */
+ persistentPaths: ['/userfs'],
+ /**
+ * @ignore
+ * @type {boolean}
+ */
+ persistentDrops: false,
+ /**
+ * @ignore
+ * @type {Array.}
+ */
+ gdextensionLibs: [],
+ /**
+ * @ignore
+ * @type {Array.}
+ */
+ fileSizes: [],
+ /**
+ * A callback function for handling Godot's ``OS.execute`` calls.
+ *
+ * This is for example used in the Web Editor template to switch between project manager and editor, and for running the game.
+ *
+ * @callback EngineConfig.onExecute
+ * @param {string} path The path that Godot's wants executed.
+ * @param {Array.} args The arguments of the "command" to execute.
+ */
+ /**
+ * @ignore
+ * @type {?function(string, Array.)}
+ */
+ onExecute: null,
+ /**
+ * A callback function for being notified when the Godot instance quits.
+ *
+ * **Note**: This function will not be called if the engine crashes or become unresponsive.
+ *
+ * @callback EngineConfig.onExit
+ * @param {number} status_code The status code returned by Godot on exit.
+ */
+ /**
+ * @ignore
+ * @type {?function(number)}
+ */
+ onExit: null,
+ /**
+ * A callback function for displaying download progress.
+ *
+ * The function is called once per frame while downloading files, so the usage of ``requestAnimationFrame()``
+ * is not necessary.
+ *
+ * If the callback function receives a total amount of bytes as 0, this means that it is impossible to calculate.
+ * Possible reasons include:
+ *
+ * - Files are delivered with server-side chunked compression
+ * - Files are delivered with server-side compression on Chromium
+ * - Not all file downloads have started yet (usually on servers without multi-threading)
+ *
+ * @callback EngineConfig.onProgress
+ * @param {number} current The current amount of downloaded bytes so far.
+ * @param {number} total The total amount of bytes to be downloaded.
+ */
+ /**
+ * @ignore
+ * @type {?function(number, number)}
+ */
+ onProgress: null,
+ /**
+ * A callback function for handling the standard output stream. This method should usually only be used in debug pages.
+ *
+ * By default, ``console.log()`` is used.
+ *
+ * @callback EngineConfig.onPrint
+ * @param {...*} [var_args] A variadic number of arguments to be printed.
+ */
+ /**
+ * @ignore
+ * @type {?function(...*)}
+ */
+ onPrint: function () {
+ console.log.apply(console, Array.from(arguments)); // eslint-disable-line no-console
+ },
+ /**
+ * A callback function for handling the standard error stream. This method should usually only be used in debug pages.
+ *
+ * By default, ``console.error()`` is used.
+ *
+ * @callback EngineConfig.onPrintError
+ * @param {...*} [var_args] A variadic number of arguments to be printed as errors.
+ */
+ /**
+ * @ignore
+ * @type {?function(...*)}
+ */
+ onPrintError: function (var_args) {
+ console.error.apply(console, Array.from(arguments)); // eslint-disable-line no-console
+ },
+ };
+
+ /**
+ * @ignore
+ * @struct
+ * @constructor
+ * @param {EngineConfig} opts
+ */
+ function Config(opts) {
+ this.update(opts);
+ }
+
+ Config.prototype = cfg;
+
+ /**
+ * @ignore
+ * @param {EngineConfig} opts
+ */
+ Config.prototype.update = function (opts) {
+ const config = opts || {};
+ // NOTE: We must explicitly pass the default, accessing it via
+ // the key will fail due to closure compiler renames.
+ function parse(key, def) {
+ if (typeof (config[key]) === 'undefined') {
+ return def;
+ }
+ return config[key];
+ }
+ // Module config
+ this.unloadAfterInit = parse('unloadAfterInit', this.unloadAfterInit);
+ this.onPrintError = parse('onPrintError', this.onPrintError);
+ this.onPrint = parse('onPrint', this.onPrint);
+ this.onProgress = parse('onProgress', this.onProgress);
+
+ // Godot config
+ this.canvas = parse('canvas', this.canvas);
+ this.executable = parse('executable', this.executable);
+ this.mainPack = parse('mainPack', this.mainPack);
+ this.locale = parse('locale', this.locale);
+ this.canvasResizePolicy = parse('canvasResizePolicy', this.canvasResizePolicy);
+ this.persistentPaths = parse('persistentPaths', this.persistentPaths);
+ this.persistentDrops = parse('persistentDrops', this.persistentDrops);
+ this.experimentalVK = parse('experimentalVK', this.experimentalVK);
+ this.focusCanvas = parse('focusCanvas', this.focusCanvas);
+ this.serviceWorker = parse('serviceWorker', this.serviceWorker);
+ this.gdextensionLibs = parse('gdextensionLibs', this.gdextensionLibs);
+ this.fileSizes = parse('fileSizes', this.fileSizes);
+ this.args = parse('args', this.args);
+ this.onExecute = parse('onExecute', this.onExecute);
+ this.onExit = parse('onExit', this.onExit);
+ };
+
+ /**
+ * @ignore
+ * @param {string} loadPath
+ * @param {Response} response
+ */
+ Config.prototype.getModuleConfig = function (loadPath, response) {
+ let r = response;
+ return {
+ 'print': this.onPrint,
+ 'printErr': this.onPrintError,
+ 'thisProgram': this.executable,
+ 'noExitRuntime': false,
+ 'dynamicLibraries': [`${loadPath}.side.wasm`],
+ 'instantiateWasm': function (imports, onSuccess) {
+ function done(result) {
+ onSuccess(result['instance'], result['module']);
+ }
+ if (typeof (WebAssembly.instantiateStreaming) !== 'undefined') {
+ WebAssembly.instantiateStreaming(Promise.resolve(r), imports).then(done);
+ } else {
+ r.arrayBuffer().then(function (buffer) {
+ WebAssembly.instantiate(buffer, imports).then(done);
+ });
+ }
+ r = null;
+ return {};
+ },
+ 'locateFile': function (path) {
+ if (!path.startsWith('godot.')) {
+ return path;
+ } else if (path.endsWith('.worker.js')) {
+ return `${loadPath}.worker.js`;
+ } else if (path.endsWith('.audio.worklet.js')) {
+ return `${loadPath}.audio.worklet.js`;
+ } else if (path.endsWith('.js')) {
+ return `${loadPath}.js`;
+ } else if (path.endsWith('.side.wasm')) {
+ return `${loadPath}.side.wasm`;
+ } else if (path.endsWith('.wasm')) {
+ return `${loadPath}.wasm`;
+ }
+ return path;
+ },
+ };
+ };
+
+ /**
+ * @ignore
+ * @param {function()} cleanup
+ */
+ Config.prototype.getGodotConfig = function (cleanup) {
+ // Try to find a canvas
+ if (!(this.canvas instanceof HTMLCanvasElement)) {
+ const nodes = document.getElementsByTagName('canvas');
+ if (nodes.length && nodes[0] instanceof HTMLCanvasElement) {
+ const first = nodes[0];
+ this.canvas = /** @type {!HTMLCanvasElement} */ (first);
+ }
+ if (!this.canvas) {
+ throw new Error('No canvas found in page');
+ }
+ }
+ // Canvas can grab focus on click, or key events won't work.
+ if (this.canvas.tabIndex < 0) {
+ this.canvas.tabIndex = 0;
+ }
+
+ // Browser locale, or custom one if defined.
+ let locale = this.locale;
+ if (!locale) {
+ locale = navigator.languages ? navigator.languages[0] : navigator.language;
+ locale = locale.split('.')[0];
+ }
+ locale = locale.replace('-', '_');
+ const onExit = this.onExit;
+
+ // Godot configuration.
+ return {
+ 'canvas': this.canvas,
+ 'canvasResizePolicy': this.canvasResizePolicy,
+ 'locale': locale,
+ 'persistentDrops': this.persistentDrops,
+ 'virtualKeyboard': this.experimentalVK,
+ 'focusCanvas': this.focusCanvas,
+ 'onExecute': this.onExecute,
+ 'onExit': function (p_code) {
+ cleanup(); // We always need to call the cleanup callback to free memory.
+ if (typeof (onExit) === 'function') {
+ onExit(p_code);
+ }
+ },
+ };
+ };
+ return new Config(initConfig);
+};
+
+/**
+ * Projects exported for the Web expose the :js:class:`Engine` class to the JavaScript environment, that allows
+ * fine control over the engine's start-up process.
+ *
+ * This API is built in an asynchronous manner and requires basic understanding
+ * of `Promises `__.
+ *
+ * @module Engine
+ * @header Web export JavaScript reference
+ */
+const Engine = (function () {
+ const preloader = new Preloader();
+
+ let loadPromise = null;
+ let loadPath = '';
+ let initPromise = null;
+
+ /**
+ * @classdesc The ``Engine`` class provides methods for loading and starting exported projects on the Web. For default export
+ * settings, this is already part of the exported HTML page. To understand practical use of the ``Engine`` class,
+ * see :ref:`Custom HTML page for Web export `.
+ *
+ * @description Create a new Engine instance with the given configuration.
+ *
+ * @global
+ * @constructor
+ * @param {EngineConfig} initConfig The initial config for this instance.
+ */
+ function Engine(initConfig) { // eslint-disable-line no-shadow
+ this.config = new InternalConfig(initConfig);
+ this.rtenv = null;
+ }
+
+ /**
+ * Load the engine from the specified base path.
+ *
+ * @param {string} basePath Base path of the engine to load.
+ * @param {number=} [size=0] The file size if known.
+ * @returns {Promise} A Promise that resolves once the engine is loaded.
+ *
+ * @function Engine.load
+ */
+ Engine.load = function (basePath, size) {
+ if (loadPromise == null) {
+ loadPath = basePath;
+ loadPromise = preloader.loadPromise(`${loadPath}.wasm`, size, true);
+ requestAnimationFrame(preloader.animateProgress);
+ }
+ return loadPromise;
+ };
+
+ /**
+ * Unload the engine to free memory.
+ *
+ * This method will be called automatically depending on the configuration. See :js:attr:`unloadAfterInit`.
+ *
+ * @function Engine.unload
+ */
+ Engine.unload = function () {
+ loadPromise = null;
+ };
+
+ /**
+ * Safe Engine constructor, creates a new prototype for every new instance to avoid prototype pollution.
+ * @ignore
+ * @constructor
+ */
+ function SafeEngine(initConfig) {
+ const proto = /** @lends Engine.prototype */ {
+ /**
+ * Initialize the engine instance. Optionally, pass the base path to the engine to load it,
+ * if it hasn't been loaded yet. See :js:meth:`Engine.load`.
+ *
+ * @param {string=} basePath Base path of the engine to load.
+ * @return {Promise} A ``Promise`` that resolves once the engine is loaded and initialized.
+ */
+ init: function (basePath) {
+ if (initPromise) {
+ return initPromise;
+ }
+ if (loadPromise == null) {
+ if (!basePath) {
+ initPromise = Promise.reject(new Error('A base path must be provided when calling `init` and the engine is not loaded.'));
+ return initPromise;
+ }
+ Engine.load(basePath, this.config.fileSizes[`${basePath}.wasm`]);
+ }
+ const me = this;
+ function doInit(promise) {
+ // Care! Promise chaining is bogus with old emscripten versions.
+ // This caused a regression with the Mono build (which uses an older emscripten version).
+ // Make sure to test that when refactoring.
+ return new Promise(function (resolve, reject) {
+ promise.then(function (response) {
+ const cloned = new Response(response.clone().body, { 'headers': [['content-type', 'application/wasm']] });
+ Godot(me.config.getModuleConfig(loadPath, cloned)).then(function (module) {
+ const paths = me.config.persistentPaths;
+ module['initFS'](paths).then(function (err) {
+ me.rtenv = module;
+ if (me.config.unloadAfterInit) {
+ Engine.unload();
+ }
+ resolve();
+ });
+ });
+ });
+ });
+ }
+ preloader.setProgressFunc(this.config.onProgress);
+ initPromise = doInit(loadPromise);
+ return initPromise;
+ },
+
+ /**
+ * Load a file so it is available in the instance's file system once it runs. Must be called **before** starting the
+ * instance.
+ *
+ * If not provided, the ``path`` is derived from the URL of the loaded file.
+ *
+ * @param {string|ArrayBuffer} file The file to preload.
+ *
+ * If a ``string`` the file will be loaded from that path.
+ *
+ * If an ``ArrayBuffer`` or a view on one, the buffer will used as the content of the file.
+ *
+ * @param {string=} path Path by which the file will be accessible. Required, if ``file`` is not a string.
+ *
+ * @returns {Promise} A Promise that resolves once the file is loaded.
+ */
+ preloadFile: function (file, path) {
+ return preloader.preload(file, path, this.config.fileSizes[file]);
+ },
+
+ /**
+ * Start the engine instance using the given override configuration (if any).
+ * :js:meth:`startGame ` can be used in typical cases instead.
+ *
+ * This will initialize the instance if it is not initialized. For manual initialization, see :js:meth:`init `.
+ * The engine must be loaded beforehand.
+ *
+ * Fails if a canvas cannot be found on the page, or not specified in the configuration.
+ *
+ * @param {EngineConfig} override An optional configuration override.
+ * @return {Promise} Promise that resolves once the engine started.
+ */
+ start: function (override) {
+ this.config.update(override);
+ const me = this;
+ return me.init().then(function () {
+ if (!me.rtenv) {
+ return Promise.reject(new Error('The engine must be initialized before it can be started'));
+ }
+
+ let config = {};
+ try {
+ config = me.config.getGodotConfig(function () {
+ me.rtenv = null;
+ });
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ // Godot configuration.
+ me.rtenv['initConfig'](config);
+
+ // Preload GDExtension libraries.
+ const libs = [];
+ if (me.config.gdextensionLibs.length > 0 && !me.rtenv['loadDynamicLibrary']) {
+ return Promise.reject(new Error('GDExtension libraries are not supported by this engine version. '
+ + 'Enable "Extensions Support" for your export preset and/or build your custom template with "dlink_enabled=yes".'));
+ }
+ me.config.gdextensionLibs.forEach(function (lib) {
+ libs.push(me.rtenv['loadDynamicLibrary'](lib, { 'loadAsync': true }));
+ });
+ return Promise.all(libs).then(function () {
+ return new Promise(function (resolve, reject) {
+ preloader.preloadedFiles.forEach(function (file) {
+ me.rtenv['copyToFS'](file.path, file.buffer);
+ });
+ preloader.preloadedFiles.length = 0; // Clear memory
+ me.rtenv['callMain'](me.config.args);
+ initPromise = null;
+ if (me.config.serviceWorker && 'serviceWorker' in navigator) {
+ navigator.serviceWorker.register(me.config.serviceWorker);
+ }
+ resolve();
+ });
+ });
+ });
+ },
+
+ /**
+ * Start the game instance using the given configuration override (if any).
+ *
+ * This will initialize the instance if it is not initialized. For manual initialization, see :js:meth:`init `.
+ *
+ * This will load the engine if it is not loaded, and preload the main pck.
+ *
+ * This method expects the initial config (or the override) to have both the :js:attr:`executable` and :js:attr:`mainPack`
+ * properties set (normally done by the editor during export).
+ *
+ * @param {EngineConfig} override An optional configuration override.
+ * @return {Promise} Promise that resolves once the game started.
+ */
+ startGame: function (override) {
+ this.config.update(override);
+ // Add main-pack argument.
+ const exe = this.config.executable;
+ const pack = this.config.mainPack || `${exe}.pck`;
+ this.config.args = ['--main-pack', pack].concat(this.config.args);
+ // Start and init with execName as loadPath if not inited.
+ const me = this;
+ return Promise.all([
+ this.init(exe),
+ this.preloadFile(pack, pack),
+ ]).then(function () {
+ return me.start.apply(me);
+ });
+ },
+
+ /**
+ * Create a file at the specified ``path`` with the passed as ``buffer`` in the instance's file system.
+ *
+ * @param {string} path The location where the file will be created.
+ * @param {ArrayBuffer} buffer The content of the file.
+ */
+ copyToFS: function (path, buffer) {
+ if (this.rtenv == null) {
+ throw new Error('Engine must be inited before copying files');
+ }
+ this.rtenv['copyToFS'](path, buffer);
+ },
+
+ /**
+ * Request that the current instance quit.
+ *
+ * This is akin the user pressing the close button in the window manager, and will
+ * have no effect if the engine has crashed, or is stuck in a loop.
+ *
+ */
+ requestQuit: function () {
+ if (this.rtenv) {
+ this.rtenv['request_quit']();
+ }
+ },
+ };
+
+ Engine.prototype = proto;
+ // Closure compiler exported instance methods.
+ Engine.prototype['init'] = Engine.prototype.init;
+ Engine.prototype['preloadFile'] = Engine.prototype.preloadFile;
+ Engine.prototype['start'] = Engine.prototype.start;
+ Engine.prototype['startGame'] = Engine.prototype.startGame;
+ Engine.prototype['copyToFS'] = Engine.prototype.copyToFS;
+ Engine.prototype['requestQuit'] = Engine.prototype.requestQuit;
+ // Also expose static methods as instance methods
+ Engine.prototype['load'] = Engine.load;
+ Engine.prototype['unload'] = Engine.unload;
+ return new Engine(initConfig);
+ }
+
+ // Closure compiler exported static methods.
+ SafeEngine['load'] = Engine.load;
+ SafeEngine['unload'] = Engine.unload;
+
+ // Feature-detection utilities.
+ SafeEngine['isWebGLAvailable'] = Features.isWebGLAvailable;
+ SafeEngine['isFetchAvailable'] = Features.isFetchAvailable;
+ SafeEngine['isSecureContext'] = Features.isSecureContext;
+ SafeEngine['isCrossOriginIsolated'] = Features.isCrossOriginIsolated;
+ SafeEngine['isSharedArrayBufferAvailable'] = Features.isSharedArrayBufferAvailable;
+ SafeEngine['isAudioWorkletAvailable'] = Features.isAudioWorkletAvailable;
+ SafeEngine['getMissingFeatures'] = Features.getMissingFeatures;
+
+ return SafeEngine;
+}());
+if (typeof window !== 'undefined') {
+ window['Engine'] = Engine;
+}
diff --git a/web-build/GodSVG.pck b/web-build/GodSVG.pck
new file mode 100644
index 000000000..2abc3206b
Binary files /dev/null and b/web-build/GodSVG.pck differ
diff --git a/web-build/GodSVG.png b/web-build/GodSVG.png
new file mode 100644
index 000000000..4d343fadc
Binary files /dev/null and b/web-build/GodSVG.png differ
diff --git a/web-build/GodSVG.wasm b/web-build/GodSVG.wasm
new file mode 100644
index 000000000..b26bd6e70
Binary files /dev/null and b/web-build/GodSVG.wasm differ
diff --git a/web-build/GodSVG.worker.js b/web-build/GodSVG.worker.js
new file mode 100644
index 000000000..c62b72af5
--- /dev/null
+++ b/web-build/GodSVG.worker.js
@@ -0,0 +1,161 @@
+/**
+ * @license
+ * Copyright 2015 The Emscripten Authors
+ * SPDX-License-Identifier: MIT
+ */
+
+// Pthread Web Worker startup routine:
+// This is the entry point file that is loaded first by each Web Worker
+// that executes pthreads on the Emscripten application.
+
+'use strict';
+
+var Module = {};
+
+// Thread-local:
+
+function assert(condition, text) {
+ if (!condition) abort('Assertion failed: ' + text);
+}
+
+function threadPrintErr() {
+ var text = Array.prototype.slice.call(arguments).join(' ');
+ console.error(text);
+}
+function threadAlert() {
+ var text = Array.prototype.slice.call(arguments).join(' ');
+ postMessage({cmd: 'alert', text: text, threadId: Module['_pthread_self']()});
+}
+// We don't need out() for now, but may need to add it if we want to use it
+// here. Or, if this code all moves into the main JS, that problem will go
+// away. (For now, adding it here increases code size for no benefit.)
+var out = () => { throw 'out() is not defined in worker.js.'; }
+var err = threadPrintErr;
+self.alert = threadAlert;
+
+Module['instantiateWasm'] = (info, receiveInstance) => {
+ // Instantiate from the module posted from the main thread.
+ // We can just use sync instantiation in the worker.
+ var instance = new WebAssembly.Instance(Module['wasmModule'], info);
+ // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193,
+ // the above line no longer optimizes out down to the following line.
+ // When the regression is fixed, we can remove this if/else.
+ receiveInstance(instance);
+ // We don't need the module anymore; new threads will be spawned from the main thread.
+ Module['wasmModule'] = null;
+ return instance.exports;
+}
+
+self.onmessage = (e) => {
+ try {
+ if (e.data.cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code.
+
+ // Module and memory were sent from main thread
+ Module['wasmModule'] = e.data.wasmModule;
+
+ Module['wasmMemory'] = e.data.wasmMemory;
+
+ Module['buffer'] = Module['wasmMemory'].buffer;
+
+ Module['ENVIRONMENT_IS_PTHREAD'] = true;
+
+ if (typeof e.data.urlOrBlob == 'string') {
+ importScripts(e.data.urlOrBlob);
+ } else {
+ var objectUrl = URL.createObjectURL(e.data.urlOrBlob);
+ importScripts(objectUrl);
+ URL.revokeObjectURL(objectUrl);
+ }
+ Godot(Module).then(function (instance) {
+ Module = instance;
+ });
+ } else if (e.data.cmd === 'run') {
+ // This worker was idle, and now should start executing its pthread entry
+ // point.
+ // performance.now() is specced to return a wallclock time in msecs since
+ // that Web Worker/main thread launched. However for pthreads this can
+ // cause subtle problems in emscripten_get_now() as this essentially
+ // would measure time from pthread_create(), meaning that the clocks
+ // between each threads would be wildly out of sync. Therefore sync all
+ // pthreads to the clock on the main browser thread, so that different
+ // threads see a somewhat coherent clock across each of them
+ // (+/- 0.1msecs in testing).
+ Module['__performance_now_clock_drift'] = performance.now() - e.data.time;
+
+ // Pass the thread address inside the asm.js scope to store it for fast access that avoids the need for a FFI out.
+ Module['__emscripten_thread_init'](e.data.threadInfoStruct, /*isMainBrowserThread=*/0, /*isMainRuntimeThread=*/0, /*canBlock=*/1);
+
+ assert(e.data.threadInfoStruct);
+ // Also call inside JS module to set up the stack frame for this pthread in JS module scope
+ Module['establishStackSpace']();
+ Module['PThread'].receiveObjectTransfer(e.data);
+ Module['PThread'].threadInit();
+
+ try {
+ // pthread entry points are always of signature 'void *ThreadMain(void *arg)'
+ // Native codebases sometimes spawn threads with other thread entry point signatures,
+ // such as void ThreadMain(void *arg), void *ThreadMain(), or void ThreadMain().
+ // That is not acceptable per C/C++ specification, but x86 compiler ABI extensions
+ // enable that to work. If you find the following line to crash, either change the signature
+ // to "proper" void *ThreadMain(void *arg) form, or try linking with the Emscripten linker
+ // flag -s EMULATE_FUNCTION_POINTER_CASTS=1 to add in emulation for this x86 ABI extension.
+ var result = Module['invokeEntryPoint'](e.data.start_routine, e.data.arg);
+
+ Module['checkStackCookie']();
+ if (Module['keepRuntimeAlive']()) {
+ Module['PThread'].setExitStatus(result);
+ } else {
+ Module['__emscripten_thread_exit'](result);
+ }
+ } catch(ex) {
+ if (ex != 'unwind') {
+ // ExitStatus not present in MINIMAL_RUNTIME
+ if (ex instanceof Module['ExitStatus']) {
+ if (Module['keepRuntimeAlive']()) {
+ err('Pthread 0x' + Module['_pthread_self']().toString(16) + ' called exit(), staying alive due to noExitRuntime.');
+ } else {
+ err('Pthread 0x' + Module['_pthread_self']().toString(16) + ' called exit(), calling _emscripten_thread_exit.');
+ Module['__emscripten_thread_exit'](ex.status);
+ }
+ }
+ else
+ {
+ // The pthread "crashed". Do not call `_emscripten_thread_exit` (which
+ // would make this thread joinable. Instead, re-throw the exception
+ // and let the top level handler propagate it back to the main thread.
+ throw ex;
+ }
+ } else {
+ // else e == 'unwind', and we should fall through here and keep the pthread alive for asynchronous events.
+ err('Pthread 0x' + Module['_pthread_self']().toString(16) + ' completed its main entry point with an `unwind`, keeping the worker alive for asynchronous operation.');
+ }
+ }
+ } else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread.
+ if (Module['_pthread_self']()) {
+ Module['__emscripten_thread_exit'](-1/*PTHREAD_CANCELED*/);
+ }
+ } else if (e.data.target === 'setimmediate') {
+ // no-op
+ } else if (e.data.cmd === 'processThreadQueue') {
+ if (Module['_pthread_self']()) { // If this thread is actually running?
+ Module['_emscripten_current_thread_process_queued_calls']();
+ }
+ } else if (e.data.cmd === 'processProxyingQueue') {
+ if (Module['_pthread_self']()) { // If this thread is actually running?
+ Module['_emscripten_proxy_execute_queue'](e.data.queue);
+ }
+ } else {
+ err('worker.js received unknown command ' + e.data.cmd);
+ err(e.data);
+ }
+ } catch(ex) {
+ err('worker.js onmessage() captured an uncaught exception: ' + ex);
+ if (ex && ex.stack) err(ex.stack);
+ if (Module['__emscripten_thread_crashed']) {
+ Module['__emscripten_thread_crashed']();
+ }
+ throw ex;
+ }
+};
+
+
diff --git a/web-build/coi-serviceworker.min.js b/web-build/coi-serviceworker.min.js
new file mode 100644
index 000000000..e79fce1c1
--- /dev/null
+++ b/web-build/coi-serviceworker.min.js
@@ -0,0 +1,2 @@
+/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
+let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const r=e.request;if("only-if-cached"===r.cache&&"same-origin"!==r.mode)return;const s=coepCredentialless&&"no-cors"===r.mode?new Request(r,{credentials:"omit"}):r;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const r=new Headers(e.headers);return r.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||r.set("Cross-Origin-Resource-Policy","cross-origin"),r.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})})).catch((e=>console.error(e))))}))):(()=>{const e={shouldRegister:()=>!0,shouldDeregister:()=>!1,coepCredentialless:()=>(window.chrome!==undefined||window.netscape!==undefined),doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator;r.serviceWorker&&r.serviceWorker.controller&&(r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:e.coepCredentialless()}),e.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})),!1===window.crossOriginIsolated&&e.shouldRegister()&&(window.isSecureContext?r.serviceWorker&&r.serviceWorker.register(window.document.currentScript.src).then((s=>{!e.quiet&&console.log("COOP/COEP Service Worker registered",s.scope),s.addEventListener("updatefound",(()=>{!e.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),e.doReload()})),s.active&&!r.serviceWorker.controller&&(!e.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),e.doReload())}),(r=>{!e.quiet&&console.error("COOP/COEP Service Worker failed to register:",r)})):!e.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})();
diff --git a/web-build/index.html b/web-build/index.html
new file mode 100644
index 000000000..561695620
--- /dev/null
+++ b/web-build/index.html
@@ -0,0 +1,248 @@
+
+
+
+
+
+ GodSVG
+
+
+
+
+
+
+
+ HTML5 canvas appears to be unsupported in the current browser.
+ Please try updating or use a different browser.
+
+
+
+
+
+
+
+
diff --git a/xport/INSTRUCTIONS.md b/xport/INSTRUCTIONS.md
new file mode 100644
index 000000000..0571725e6
--- /dev/null
+++ b/xport/INSTRUCTIONS.md
@@ -0,0 +1,41 @@
+This is a tutorial on exporting with a custom template, which makes the executable smaller by removing unused features. It might be a bad way of doing things. I can only hope the open-sourcedness of the project will help polish up the workflow eventually.
+
+## Initial setup
+
+This will be needed the first time.
+
+- Clone or fork the Godot repository: https://github.com/godotengine/godot
+- Set up scons.
+- For each platform you want to export for, read the documentation on how to set up scons for it. For example, https://docs.godotengine.org/en/latest/contributing/development/compiling/compiling_for_windows.html#cross-compiling-for-windows-from-other-operating-systems explains how to setup the windows platform from Linux. Use `scons p=list` to check if the platform is set up.
+
+## Exporting
+
+- While in a CLI, go in the root folder of the Godot source code.
+- Sync the local repo. For a minor version, fetching its branch might be necessary, i.e. `git fetch upstream 4.2`
+- Find the commit hash of the Godot version GodSVG is using: https://github.com/godotengine/godot/releases
+- `git checkout `
+- Run the scons command; see below.
+- Wait for the compilation to finish. The template will be in the bin directory, but avoid moving it inside the GodSVG project.
+
+For most platforms, to export, you'd need to find the relevant template in your file system to fill in the Custom Template field, then use "Export Project".
+
+For official web exports, after this is done, you'll get a lot of files. Normally, only `web-build/GodSVG.pck` needs to be changed to the new .pck file between builds. `web-build/GodSVG.wasm` file may need to be updated too if there's a new version of Godot.
+
+If web exports need to be tested without pushing any changes to the repository, you should run `python3 -m http.server` to run a local server inside the root folder (it won't have any effect on the git repository) and then visit the `http://localhost:8000/web-build/` URL.
+
+## Commands
+
+Profile path should be adjusted
+
+- `scons p=linuxbsd arch=x86_64 optimize=speed profile=../GodSVG/xport/custom.py`
+
+- `scons p=windows arch=x86_64 optimize=speed profile=../GodSVG/xport/custom.py`
+
+- `scons p=web arch=wasm32 optimize=size javascript_eval=no profile=../GodSVG/xport/custom.py`
+
+(If the web one doesn't work, type it out - currently that's `scons p=web arch=wasm32 javascript_eval=no target=template_release lto=full production=yes dev_build=no optimize=size deprecated=no minizip=no brotli=no vulkan=no openxr=no use_volk=no disable_3d=yes modules_enabled_by_default=yes module_freetype_enabled=yes module_gdscript_enabled=yes module_svg_enabled=yes module_jpg_enabled=yes module_text_server_adv_enabled=yes graphite=no module_webp_enabled=yes`)
+
+## Misc:
+
+`scons --help` on the godot source lists its modules.
+
diff --git a/xport/custom.py b/xport/custom.py
new file mode 100644
index 000000000..7e8f85a30
--- /dev/null
+++ b/xport/custom.py
@@ -0,0 +1,19 @@
+target="template_release"
+lto="full"
+production="yes"
+dev_build="no"
+deprecated="no"
+minizip="no"
+brotli="no"
+vulkan="no"
+openxr="no"
+use_volk="no"
+disable_3d="yes"
+modules_enabled_by_default="no"
+module_freetype_enabled="yes"
+module_gdscript_enabled="yes"
+module_svg_enabled="yes"
+module_jpg_enabled="yes"
+module_text_server_adv_enabled="yes"
+graphite="no"
+module_webp_enabled="yes"