Compare commits
49 Commits
build-0be9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 773867bd2c | |||
| 760ffb8b80 | |||
| 853e4423f7 | |||
| fc6fa30c85 | |||
| 95476cfd57 | |||
| 722fed9b94 | |||
| e8620611df | |||
| 85a208a729 | |||
| 92fc5dda89 | |||
| af17a1dee0 | |||
| 43dfc410dc | |||
| ef8ecb3732 | |||
| 803e867e20 | |||
| cebef2f06b | |||
| e0420a6583 | |||
| 56e76c61e6 | |||
| 5dfe68cb81 | |||
| 6e02a67114 | |||
| 32a67a6617 | |||
| fb0c7a9e3b | |||
| f0ce00b23b | |||
| d4a9f5a0b0 | |||
| 085d6a21a0 | |||
| 99d2d3f508 | |||
| dad22ece22 | |||
| 52496df10e | |||
| 29b903fadd | |||
| e8191e2de8 | |||
| b7b7d7cdec | |||
| fc6e4a1028 | |||
| 1dfe21363b | |||
| fde3f23ed5 | |||
| 79e54c28da | |||
| a8f327ae5a | |||
| 1295bcbcba | |||
| 758f2ca885 | |||
| 524920d140 | |||
| f9c72448d9 | |||
|
|
135e63ef52 | ||
|
|
c86e68d383 | ||
| f226c14c00 | |||
| d345e700fc | |||
| bc187cff09 | |||
| fc4cacdc9f | |||
| 848361d19b | |||
| 711865c6a4 | |||
| d231b730f7 | |||
| 8ba361f3c2 | |||
| c74dccf1cf |
80
.github/workflows/build-windows.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
||||
export-windows:
|
||||
name: Windows Export
|
||||
runs-on: self-hosted
|
||||
if: contains(github.event.head_commit.message, '!build')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -99,6 +100,74 @@ jobs:
|
||||
runs-on: self-hosted
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push' && success()
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate Release Info
|
||||
id: release_info
|
||||
run: |
|
||||
# Get current date and time
|
||||
$date = Get-Date -Format "yyyy.MM.dd"
|
||||
$time = Get-Date -Format "HHmm"
|
||||
|
||||
# Get short commit hash (first 7 characters)
|
||||
$shortSha = "${{ github.sha }}".Substring(0, 7)
|
||||
|
||||
# Get commit count since last tag (for build number)
|
||||
$buildNumber = & git rev-list --count HEAD
|
||||
|
||||
# Create version-like tag and release name
|
||||
$tag = "v$date-build$buildNumber"
|
||||
$releaseName = "PNGTuber+ v$date Build $buildNumber"
|
||||
|
||||
Write-Host "Generated tag: $tag"
|
||||
Write-Host "Generated release name: $releaseName"
|
||||
|
||||
# Get the latest release tag to compare against
|
||||
$latestTag = ""
|
||||
try {
|
||||
$latestTag = & git describe --tags --abbrev=0 2>$null
|
||||
Write-Host "Latest tag found: $latestTag"
|
||||
} catch {
|
||||
Write-Host "No previous tags found, showing all commits"
|
||||
}
|
||||
|
||||
# Generate commit history since last tag (or all commits if no tag)
|
||||
$commitRange = if ($latestTag) { "$latestTag..HEAD" } else { "HEAD" }
|
||||
|
||||
# Get commits with format: short hash, subject, author
|
||||
$commits = & git log $commitRange --pretty=format:"%h|%s|%an" --no-merges
|
||||
|
||||
# Build commit history as a single string
|
||||
$commitHistory = ""
|
||||
|
||||
if ($commits) {
|
||||
$commitHistory += "`n`n## Changes Since Last Release`n`n"
|
||||
|
||||
$commits | ForEach-Object {
|
||||
$parts = $_ -split '\|', 3
|
||||
$hash = $parts[0]
|
||||
$subject = $parts[1] -replace '"', '""'
|
||||
$author = $parts[2]
|
||||
$commitUrl = "https://github.com/${{ github.repository }}/commit/$hash"
|
||||
$commitHistory += "- [$hash]($commitUrl) $subject`n"
|
||||
}
|
||||
} else {
|
||||
$commitHistory += "`n`n## Changes Since Last Release`n`nNo new commits since last release.`n"
|
||||
}
|
||||
|
||||
# Set outputs for use in next steps
|
||||
echo "tag=$tag" >> $env:GITHUB_OUTPUT
|
||||
echo "release_name=$releaseName" >> $env:GITHUB_OUTPUT
|
||||
echo "short_sha=$shortSha" >> $env:GITHUB_OUTPUT
|
||||
echo "build_number=$buildNumber" >> $env:GITHUB_OUTPUT
|
||||
|
||||
# Use heredoc syntax for multiline commit history
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value "commit_history<<EOF"
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value $commitHistory
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value "EOF"
|
||||
- name: Download Windows Build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -111,21 +180,18 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: build-${{ github.sha }}
|
||||
release_name: PNGTuber Plus Build ${{ github.sha }}
|
||||
tag_name: ${{ steps.release_info.outputs.tag }}
|
||||
release_name: ${{ steps.release_info.outputs.release_name }}
|
||||
body: |
|
||||
Automated build of PNGTuber Plus
|
||||
|
||||
## Recent Updates by Litruv
|
||||
- **StreamDeck Integration**: Add buttons to enable the streamdeck plugin that was built in
|
||||
- **Rotation Wobble**: New sprite rotation effects with frequency and amplitude controls
|
||||
- **Enhanced Modifier Keys**: Support for complex key combinations (Ctrl+Shift+Alt+Cmd)
|
||||
- **Godot 4.4.1 Support**: Updated for latest Godot stable release
|
||||
${{ steps.release_info.outputs.commit_history }}
|
||||
|
||||
## Installation
|
||||
1. Download PNGTuber-Plus.exe from the assets below
|
||||
2. Run the executable - no installation required!
|
||||
3. For StreamDeck integration, download the plugin from [here](https://github.com/BoyneGames/streamdeck-godot-plugin/releases/tag/1.0.0)
|
||||
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
|
||||
3
.gitignore
vendored
@@ -3,4 +3,5 @@
|
||||
.export/
|
||||
.itchio/
|
||||
.steam/
|
||||
build/
|
||||
build/
|
||||
*.TMP
|
||||
|
||||
11
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build PNGTuber-Plus",
|
||||
"type": "shell",
|
||||
"command": "cd c:\\DEV\\Sync\\PNGTuber-Plus; .\\build-windows.bat",
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
154
NDI_SETUP_GUIDE.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# NDI Output for PNGTuber-Plus
|
||||
|
||||
This guide explains how to use the NDI (Network Device Interface) output feature in PNGTuber-Plus to stream your avatar to OBS Studio or other NDI-compatible applications.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. **NDI Runtime**: Download and install the NDI Runtime from [NDI website](http://ndi.link/NDIRedistV6)
|
||||
- Windows: [NDI Runtime for Windows](http://ndi.link/NDIRedistV6)
|
||||
- macOS: [NDI Runtime for macOS](http://ndi.link/NDIRedistV6Apple)
|
||||
- Linux: [NDI Runtime for Linux](https://github.com/DistroAV/DistroAV/wiki/1.-Installation#linux) (Note: Flatpak incompatible)
|
||||
|
||||
2. **Godot 4.4-stable or later**: This plugin requires Godot 4.4+ to function properly
|
||||
|
||||
3. **NDI-compatible software**: Such as OBS Studio with NDI plugin, vMix, Wirecast, etc.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time NDI streaming**: Stream your PNGTuber avatar over the network
|
||||
- **Audio support**: Include system audio in the NDI stream (optional)
|
||||
- **Customizable source name**: Set a custom name for your NDI source
|
||||
- **Hotkey controls**: Quick toggle streaming with keyboard shortcuts
|
||||
- **Settings UI**: Easy configuration through the settings menu
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Basic Setup
|
||||
|
||||
The NDI output is automatically enabled when you start PNGTuber-Plus. The default settings are:
|
||||
- **Source Name**: "PNGTuber-Plus"
|
||||
- **Audio**: Enabled (Master bus)
|
||||
- **Auto-start**: Enabled
|
||||
|
||||
### 2. Settings Menu Configuration
|
||||
|
||||
Access the NDI settings through the Settings Menu:
|
||||
|
||||
1. Click the settings wheel in PNGTuber-Plus
|
||||
2. Scroll down to find the "NDI streaming" section
|
||||
3. Configure the following options:
|
||||
- **NDI streaming**: Enable/disable NDI output
|
||||
- **Source name**: Set a custom name for your NDI source (default: "PNGTuber-Plus")
|
||||
- **Include audio**: Toggle audio streaming on/off
|
||||
- **Avatar-only stream**: Enable a clean avatar-only stream without UI elements
|
||||
- **Status**: View current streaming status
|
||||
- **Start/Stop NDI**: Manual control button
|
||||
|
||||
#### Avatar-Only Streaming
|
||||
|
||||
The "Avatar-only stream" option creates a second NDI source called "PNGTuber Avatar" that:
|
||||
- Streams only the avatar sprites with a transparent background
|
||||
- Excludes all UI elements, menus, and controls
|
||||
- Perfect for professional streaming setups
|
||||
- Syncs automatically with the main avatar state
|
||||
|
||||
### 3. Hotkey Controls
|
||||
|
||||
Quick keyboard shortcuts for NDI control:
|
||||
- **F9**: Toggle NDI streaming on/off
|
||||
- **F10**: Restart NDI streaming (useful for applying new settings)
|
||||
|
||||
### 4. OBS Studio Setup
|
||||
|
||||
To capture the NDI stream in OBS Studio:
|
||||
|
||||
1. **Install NDI Plugin for OBS**:
|
||||
- Download from [OBS NDI Plugin](https://github.com/Palakis/obs-ndi/releases)
|
||||
- Install following the provided instructions
|
||||
|
||||
2. **Add NDI Source in OBS**:
|
||||
- In OBS, click the "+" in Sources
|
||||
- Select "NDI Source"
|
||||
- Name your source (e.g., "PNGTuber Avatar")
|
||||
- In the properties, select your NDI source from the dropdown
|
||||
- Look for "PNGTuber-Plus" (or your custom name)
|
||||
- Configure audio settings as needed
|
||||
|
||||
3. **Position and Scale**:
|
||||
- Resize and position the NDI source in your scene
|
||||
- The stream will include the full PNGTuber viewport
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### NDI Source Not Appearing in OBS
|
||||
|
||||
1. **Check NDI Runtime**: Ensure NDI Runtime is installed on both machines
|
||||
2. **Network connectivity**: NDI works over network, ensure devices can communicate
|
||||
3. **Firewall settings**: NDI uses specific ports, check firewall rules
|
||||
4. **Restart applications**: Try restarting both PNGTuber-Plus and OBS
|
||||
5. **Check source name**: Verify the NDI source name matches in settings
|
||||
|
||||
### Audio Issues
|
||||
|
||||
1. **Audio bus**: Ensure the correct audio bus is selected (default: "Master")
|
||||
2. **System audio**: Check that PNGTuber-Plus is receiving audio input
|
||||
3. **OBS audio settings**: Verify NDI source audio is not muted in OBS
|
||||
|
||||
### Performance Issues
|
||||
|
||||
1. **Network bandwidth**: NDI requires good network performance for quality streaming
|
||||
2. **Local streaming**: For best performance, use NDI on the same machine
|
||||
3. **Resolution**: Lower viewport resolution if experiencing lag
|
||||
4. **Frame rate**: Adjust FPS settings in PNGTuber-Plus settings
|
||||
|
||||
### NDI Not Working
|
||||
|
||||
1. **Godot version**: Ensure you're using Godot 4.4-stable or later
|
||||
2. **Plugin enabled**: Verify the godot-ndi plugin is properly installed
|
||||
3. **System compatibility**: Check NDI Runtime compatibility with your OS
|
||||
4. **Console errors**: Check Godot console for any NDI-related errors
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Audio Bus
|
||||
|
||||
You can route specific audio to NDI by:
|
||||
1. Creating a custom audio bus in PNGTuber-Plus
|
||||
2. Setting the NDI audio bus to your custom bus name
|
||||
3. Routing desired audio sources to that bus
|
||||
|
||||
### Network Performance
|
||||
|
||||
For optimal network streaming:
|
||||
- Use wired connections when possible
|
||||
- Ensure sufficient bandwidth (>100 Mbps recommended for high quality)
|
||||
- Keep NDI devices on the same network segment
|
||||
- Consider NDI quality settings if available
|
||||
|
||||
### Multiple Instances
|
||||
|
||||
You can run multiple PNGTuber-Plus instances with different NDI source names:
|
||||
1. Change the "Source name" in each instance
|
||||
2. Each will appear as a separate NDI source
|
||||
3. Useful for multiple avatars or backup streams
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Video Format**: NDI handles video format automatically based on viewport
|
||||
- **Audio Format**: Uses the selected audio bus output
|
||||
- **Compression**: NDI uses efficient compression for network streaming
|
||||
- **Latency**: Typically very low latency (<1 frame) on local network
|
||||
|
||||
## Legal Notice
|
||||
|
||||
NDI® is a registered trademark of Vizrt NDI AB. Your application must comply with the [NDI SDK license](http://ndi.link/ndisdk_license) when using this feature.
|
||||
|
||||
## Support
|
||||
|
||||
For issues specific to NDI functionality:
|
||||
1. Check the console output for error messages
|
||||
2. Verify NDI Runtime installation
|
||||
3. Test with other NDI applications to isolate issues
|
||||
4. Check network connectivity for remote streaming
|
||||
|
||||
For PNGTuber-Plus specific issues, refer to the main application documentation and support channels.
|
||||
98
README.md
@@ -2,15 +2,93 @@
|
||||
|
||||

|
||||
|
||||
### Recent Updates by Litruv
|
||||
- **GitHub Actions**: Automated Windows builds on every commit
|
||||
- **StreamDeck Integration**: Add buttons to enable the streamdeck plugin that was built in
|
||||
- **Rotation Wobble**: New sprite rotation effects with frequency and amplitude controls
|
||||
- **Enhanced Modifier Keys**: Support for complex key combinations (Ctrl+Shift+Alt+Cmd)
|
||||
- **Godot 4.4.1 Support**: Updated for latest Godot stable release
|
||||
A fork of [kaiakairos/PNGTuber-Plus](https://github.com/kaiakairos/PNGTuber-Plus) with some quality-of-life upgrades, editor polish, and a few power-user toys.
|
||||
|
||||
## 🙏 Credits
|
||||
---
|
||||
|
||||
- **Original Project**: PNGTuber Plus base application
|
||||
- **StreamDeck Plugin**: Based on [BoyneGames StreamDeck Godot Plugin](https://github.com/BoyneGames/streamdeck-godot-plugin)
|
||||
- **Enhanced Features**: Max Litruv Boonzaayer
|
||||
## 🔧 Fork Changes (Litruv Edition)
|
||||
|
||||
### 🎛️ Opacity Controls
|
||||
|
||||
Added a proper opacity slider for sprites, with an "affect children" toggle. Clip masks still behave like they should.
|
||||
|
||||

|
||||
|
||||
|
||||
### 🎨 Blend Modes
|
||||
|
||||
Photoshop-style blend modes for sprites! Choose from Normal, Multiply, Screen, Overlay, Soft Light, Hard Light, Color Dodge, Color Burn, Darken, Lighten, Add, and Subtract. Perfect for lighting effects, shadows, or artistic styling.
|
||||
|
||||
* Dropdown selector in the sprite viewer menu
|
||||
* Works alongside opacity controls
|
||||
|
||||
|
||||
### 🌀 Rotation Wobble
|
||||
|
||||
Sprites can now do a subtle (or chaotic) rotational wobble. Frequency + amplitude sliders included.
|
||||
|
||||

|
||||
|
||||
|
||||
### 🔗 Wobble Sync Groups
|
||||
|
||||
Keep multiple sprites in sync with wobble sync groups. Create groups, assign sprites, and all wobble parameters (X/Y/Rotation frequency, amplitude) automatically sync across group members. Perfect for ears, hair, accessories, or any elements that should move together.
|
||||
|
||||
* Create custom sync groups with descriptive names
|
||||
* Assign multiple sprites to the same group
|
||||
* Edit any group member's wobble settings to update the entire group
|
||||
* Visual indicators show which sprites are synced
|
||||
* Groups persist in save files
|
||||
|
||||
|
||||
### ⌨️ Enhanced Modifier Keys
|
||||
|
||||
Supports combos like Ctrl+Shift+Alt+Cmd. For people with more keybinds than fingers.
|
||||
|
||||

|
||||
|
||||
|
||||
### ↗️ Editor Middle Mouse Button Panning
|
||||
|
||||
Hold MMB in the editor to pan around the scene, it'll reset when you go back to live.
|
||||
|
||||

|
||||
|
||||
### ↗️ View Mode MMB Shake
|
||||
|
||||
Hold MMB in view mode to shake your character, Show off them physics!
|
||||
|
||||

|
||||
|
||||
|
||||
### 🎞️ Delta Time Physics
|
||||
|
||||
Physics are now framerate-independent. Low FPS won’t make your character move in slow motion anymore.
|
||||
Just smooth, consistent jank if that’s your thing. Great for stop-motion vibes.
|
||||
|
||||
|
||||
### 🎤 Microphone Fixes
|
||||
|
||||
Merged in [k0ffinz’s](https://github.com/k0ffinz/PNGTuber-Plus) audio tweaks:
|
||||
|
||||
* Reduced mic startup delay
|
||||
* Better loudness detection
|
||||
* General stability improvements
|
||||
|
||||
|
||||
### 🧱 Engine Update
|
||||
|
||||
Updated to **Godot 4.4.1** — no more editor nags or compatibility weirdness.
|
||||
|
||||
|
||||
### 🎚️ StreamDeck Integration
|
||||
|
||||
StreamDeck plugin was already in, this just wires up the buttons.
|
||||
|
||||
---
|
||||
|
||||
## 🧾 Credits
|
||||
|
||||
* Original project by [kaiakairos](https://github.com/kaiakairos/PNGTuber-Plus)
|
||||
* StreamDeck plugin from [BoyneGames](https://github.com/BoyneGames/streamdeck-godot-plugin)
|
||||
* Mic improvements by [k0ffinz](https://github.com/k0ffinz/PNGTuber-Plus)
|
||||
|
||||
@@ -4,4 +4,4 @@ name="Godot Git Plugin"
|
||||
description="This plugin lets you interact with Git without leaving the Godot editor. More information can be found at https://github.com/godotengine/godot-git-plugin/wiki"
|
||||
author="twaritwaikar"
|
||||
version="v3.1.0"
|
||||
script="godot-git-plugin.gd"
|
||||
script=""
|
||||
|
||||
379
addons/godot-ndi/LICENSE
Normal file
@@ -0,0 +1,379 @@
|
||||
|
||||
https://github.com/unvermuthet/godot-ndi
|
||||
|
||||
(C) 2025 Henry Muth - unvermuthet
|
||||
|
||||
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
52
addons/godot-ndi/NOTICE.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Third-party software
|
||||
|
||||
## NDI SDK
|
||||
|
||||
- `ndi/*.h`
|
||||
|
||||
### MIT License
|
||||
|
||||
> The following MIT license applies to these files ONLY and not to the SDK as a whole. Please review the SDK documentation for the description of the full license terms, which are also provided in the file "NDI License Agreement.pdf" within the SDK or online at http://ndi.link/ndisdk_license. Your use of any part of this SDK is acknowledgment that you agree to the SDK license terms. The full NDI SDK may be downloaded at http://ndi.video/
|
||||
|
||||
Copyright (C) 2023-2024 Vizrt NDI AB. All rights reserved.
|
||||
|
||||
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.
|
||||
|
||||
## godot-cpp
|
||||
|
||||
- `godot-cpp/**`
|
||||
|
||||
### MIT License
|
||||
|
||||
Copyright (c) 2017-present Godot Engine 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.
|
||||
62
addons/godot-ndi/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# godot-ndi [](https://godotengine.org) [](https://github.com/unvermuthet/godot-ndi/actions/workflows/builds.yml)
|
||||
|
||||
> Integrates the NDI® SDK with Godot
|
||||
>
|
||||
> [ NDI® is a registered trademark of Vizrt NDI AB ]
|
||||
|
||||
Adds a new [VideoStream](https://godot-ndi.readthedocs.io/stable/classes/class_videostreamndi.html) class to implement **NDI playback**.
|
||||
For **outputting the Viewport** as an NDI source, add the [NDIOutput](https://godot-ndi.readthedocs.io/stable/classes/class_ndioutput.html) Node to the scene.
|
||||
Check out the **demo scenes** included with all releases.
|
||||
|
||||
This was born out of work on my DMX Lighting Control Software. I'm fascinated by Live Event Productions, Projection Mapping, TechArt, and Broadcasting. [So reach out](https://discord.com/users/203583245223198722) or [show off](https://github.com/unvermuthet/godot-ndi/discussions/categories/show-and-tell) what you made with Godot NDI!
|
||||
|
||||
<!-- [](https://discord.com/users/203583245223198722) -->
|
||||
|
||||

|
||||
|
||||
> This GIF only shows local transmission, but imagine this over the network!
|
||||
|
||||
## Install
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - Only compatible with `Godot 4.4-stable` and later
|
||||
> - Your application needs to comply with the [NDI SDK license](http://ndi.link/ndisdk_license).
|
||||
> - The NDI Runtime is required ([Windows](http://ndi.link/NDIRedistV6), [MacOS](http://ndi.link/NDIRedistV6Apple), [Linux (Flatpak incompatible)](https://github.com/DistroAV/DistroAV/wiki/1.-Installation#linux))
|
||||
|
||||
Download the latest version under `Releases` and install by extracting the ZIP file in your Godot project.
|
||||
Alternatively, you can install the extension from Godot's Asset Library.
|
||||
|
||||
## Support
|
||||
|
||||
For ***commercial*** support, exclusive alternative licensing, or [NDI Advanced SDK](https://ndi.video/for-developers/ndi-advanced/) integration, contact me at [godot-ndi@muth.pro](mailto:godot-ndi@muth.pro).
|
||||
|
||||
In the production industries, software usually comes with exorbitant licensing fees. If this project has been useful to you and you want to show appreciation for my time and effort, consider a donation.
|
||||
|
||||
[](https://ko-fi.com/I2I51A7ZC3)
|
||||
|
||||
## Building from source
|
||||
|
||||
To help you get going, I've configured a [Development Container](https://containers.dev/) with everything set up to target Windows or Linux. Just run `scons` or check `scons --help`!
|
||||
|
||||
If you want to configure the environment yourself, follow [Godot's "Building from source" Guide](https://docs.godotengine.org/en/latest/contributing/development/compiling/). This project uses an identical build system.
|
||||
|
||||
## License
|
||||
|
||||
**(C) 2025-present Henry Muth - unvermuthet and Godot NDI contributors**
|
||||
|
||||
### Software
|
||||
|
||||
Subject to the terms of the [Mozilla Public License, v. 2.0](https://www.mozilla.org/en-US/MPL/2.0/)
|
||||
|
||||
### Documentation
|
||||
|
||||
Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
### Attribution suggestion
|
||||
|
||||
```
|
||||
godot-ndi
|
||||
Licensed under MPL-2.0
|
||||
(C) 2025-present Henry Muth - unvermuthet and Godot NDI contributors
|
||||
https://github.com/unvermuthet/godot-ndi
|
||||
```
|
||||
73
addons/godot-ndi/demo/3DOutput.tscn
Normal file
@@ -0,0 +1,73 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://deioml0m7ep1t"]
|
||||
|
||||
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_4r73q"]
|
||||
emission_shape = 1
|
||||
emission_sphere_radius = 1.0
|
||||
direction = Vector3(0, 1, 0)
|
||||
initial_velocity_min = 2.0
|
||||
initial_velocity_max = 6.0
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_jpqj6"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_ln6fu"]
|
||||
font_size = 24
|
||||
shadow_size = 5
|
||||
shadow_color = Color(0, 0, 0, 0.235294)
|
||||
|
||||
[node name="3DOutput" type="SubViewportContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
stretch = true
|
||||
|
||||
[node name="Node3D" type="Node3D" parent="."]
|
||||
|
||||
[node name="GPUParticles3D" type="GPUParticles3D" parent="Node3D"]
|
||||
amount = 32
|
||||
lifetime = 2.0
|
||||
process_material = SubResource("ParticleProcessMaterial_4r73q")
|
||||
draw_pass_1 = SubResource("SphereMesh_jpqj6")
|
||||
|
||||
[node name="SubViewportB" type="SubViewport" parent="."]
|
||||
handle_input_locally = false
|
||||
size = Vector2i(1152, 648)
|
||||
render_target_update_mode = 4
|
||||
|
||||
[node name="NDIOutput" type="NDIOutput" parent="SubViewportB"]
|
||||
name = "Godot Cam B"
|
||||
enable_editor_output = true
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="SubViewportB"]
|
||||
transform = Transform3D(1, 0, 0, 0, -0.0010647, 0.999999, 0, -0.999999, -0.0010647, 0, 6.41845, 0)
|
||||
current = true
|
||||
|
||||
[node name="SubViewportA" type="SubViewport" parent="."]
|
||||
handle_input_locally = false
|
||||
size = Vector2i(1152, 648)
|
||||
render_target_update_mode = 4
|
||||
|
||||
[node name="NDIOutput" type="NDIOutput" parent="SubViewportA"]
|
||||
name = "Godot Cam A"
|
||||
enable_editor_output = true
|
||||
|
||||
[node name="Label" type="Label" parent="SubViewportA"]
|
||||
anchors_preset = 2
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 31.0
|
||||
offset_top = -357.0
|
||||
offset_right = 534.0
|
||||
offset_bottom = -27.0
|
||||
grow_vertical = 0
|
||||
text = "NDIOutput always transmits the Viewport closest up the Tree. When running in the Editor that's the 2D View.
|
||||
|
||||
If you want to display a camera view, add NDIOutput to a SubViewport instead. You can skip the SubViewportContainer incase you don't need the Scene to be visible when running the Game. "
|
||||
label_settings = SubResource("LabelSettings_ln6fu")
|
||||
horizontal_alignment = 3
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="SubViewportA"]
|
||||
transform = Transform3D(0.99299, -0.0520139, 0.106136, 0, 0.897966, 0.440065, -0.118196, -0.43698, 0.891672, -2.28829, 1.75016, 5.03714)
|
||||
current = true
|
||||
58
addons/godot-ndi/demo/AnimationOutput.tscn
Normal file
@@ -0,0 +1,58 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://p6ywmqtt7nke"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://cfvxj5cdj3n45" path="res://addons/godot-ndi/demo/assets/icon.svg" id="1_ekn6m"]
|
||||
|
||||
[sub_resource type="Animation" id="Animation_62e2m"]
|
||||
length = 0.001
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Icon:position")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 0,
|
||||
"values": [Vector2(1034, 527)]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_0fbet"]
|
||||
resource_name = "new_animation"
|
||||
length = 8.0
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Icon:position")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 3.9627, 7.9587),
|
||||
"transitions": PackedFloat32Array(1, 1, 1),
|
||||
"update": 0,
|
||||
"values": [Vector2(116, 119), Vector2(1034, 527), Vector2(116, 119)]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_ee4bf"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_62e2m"),
|
||||
&"new_animation": SubResource("Animation_0fbet")
|
||||
}
|
||||
|
||||
[node name="SubViewport" type="Node2D"]
|
||||
|
||||
[node name="NDIOutput" type="NDIOutput" parent="."]
|
||||
name = "Godot (Animation)"
|
||||
|
||||
[node name="Icon" type="Sprite2D" parent="."]
|
||||
position = Vector2(1034, 527)
|
||||
scale = Vector2(1.463, 1.463)
|
||||
texture = ExtResource("1_ekn6m")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_ee4bf")
|
||||
}
|
||||
autoplay = "new_animation"
|
||||
28
addons/godot-ndi/demo/Finder.tscn
Normal file
@@ -0,0 +1,28 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dydvtidqppffs"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://kbgdevvugj7k" path="res://addons/godot-ndi/demo/ndi_finder.gd" id="1_0rvup"]
|
||||
|
||||
[node name="Canvas" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
autoplay = true
|
||||
expand = true
|
||||
|
||||
[node name="NDIFinder" type="NDIFinder" parent="."]
|
||||
script = ExtResource("1_0rvup")
|
||||
|
||||
[connection signal="sources_changed" from="NDIFinder" to="NDIFinder" method="_on_sources_changed"]
|
||||
[connection signal="sources_found" from="NDIFinder" to="NDIFinder" method="_on_sources_found"]
|
||||
[connection signal="sources_gone" from="NDIFinder" to="NDIFinder" method="_on_sources_gone"]
|
||||
22
addons/godot-ndi/demo/VideoInput.tscn
Normal file
@@ -0,0 +1,22 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://byblss3lhwvqm"]
|
||||
|
||||
[sub_resource type="VideoStreamNDI" id="VideoStreamNDI_lv7vm"]
|
||||
|
||||
[node name="Canvas" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
stream = SubResource("VideoStreamNDI_lv7vm")
|
||||
autoplay = true
|
||||
expand = true
|
||||
37
addons/godot-ndi/demo/VideoOutput.tscn
Normal file
@@ -0,0 +1,37 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://ddr3hiaft61tl"]
|
||||
|
||||
[ext_resource type="VideoStream" uid="uid://0whnog13yrlf" path="res://addons/godot-ndi/demo/assets/liftoff_space_shuttle_atlantis.ogv" id="1_utmvk"]
|
||||
[ext_resource type="AudioStream" uid="uid://dyvxmok1n3vu1" path="res://addons/godot-ndi/demo/assets/sub-d-more-jazz-guitar.wav" id="2_51pmd"]
|
||||
[ext_resource type="Script" uid="uid://d2sex4jisnkck" path="res://addons/godot-ndi/demo/video_stream_player.gd" id="2_w6vuq"]
|
||||
[ext_resource type="VideoStream" path="res://addons/godot-ndi/demo/assets/twitch_sync_footage.ogv" id="3_ug8g3"]
|
||||
|
||||
[node name="Canvas" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
volume_db = -15.0
|
||||
autoplay = true
|
||||
script = ExtResource("2_w6vuq")
|
||||
playlist = Array[VideoStream]([ExtResource("1_utmvk"), ExtResource("3_ug8g3")])
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer2D" parent="."]
|
||||
stream = ExtResource("2_51pmd")
|
||||
autoplay = true
|
||||
panning_strength = 0.0
|
||||
|
||||
[node name="NDIOutput" type="NDIOutput" parent="."]
|
||||
name = "Godot (Video)"
|
||||
audio_bus = &"Master"
|
||||
|
||||
[connection signal="finished" from="VideoStreamPlayer" to="VideoStreamPlayer" method="_on_finished"]
|
||||
1
addons/godot-ndi/demo/assets/icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||
|
After Width: | Height: | Size: 994 B |
37
addons/godot-ndi/demo/assets/icon.svg.import
Normal file
@@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cfvxj5cdj3n45"
|
||||
path="res://.godot/imported/icon.svg-5dbb75b05306facc4cf78b15baaae64d.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot-ndi/demo/assets/icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-5dbb75b05306facc4cf78b15baaae64d.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
|
||||
BIN
addons/godot-ndi/demo/assets/liftoff_space_shuttle_atlantis.ogv
Normal file
@@ -0,0 +1 @@
|
||||
uid://0whnog13yrlf
|
||||
BIN
addons/godot-ndi/demo/assets/sub-d-more-jazz-guitar.wav
Normal file
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dyvxmok1n3vu1"
|
||||
path="res://.godot/imported/sub-d-more-jazz-guitar.wav-6bba7f91e7070b729764277f88bb96b0.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot-ndi/demo/assets/sub-d-more-jazz-guitar.wav"
|
||||
dest_files=["res://.godot/imported/sub-d-more-jazz-guitar.wav-6bba7f91e7070b729764277f88bb96b0.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=true
|
||||
edit/loop_mode=2
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
addons/godot-ndi/demo/assets/twitch_sync_footage.ogv
Normal file
1
addons/godot-ndi/demo/assets/twitch_sync_footage.ogv.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://4mp8kyfc212k
|
||||
13
addons/godot-ndi/demo/ndi_finder.gd
Normal file
@@ -0,0 +1,13 @@
|
||||
extends NDIFinder
|
||||
|
||||
@onready var player: VideoStreamPlayer = $"../VideoStreamPlayer"
|
||||
|
||||
func _on_sources_changed() -> void:
|
||||
player.stream = get_sources().pop_front()
|
||||
player.play()
|
||||
|
||||
func _on_sources_found(found_sources: Array[VideoStreamNDI]) -> void:
|
||||
print("NDI sources found: ", found_sources.map(func (source: VideoStreamNDI): return source.get_name()))
|
||||
|
||||
func _on_sources_gone(gone_sources: Array[VideoStreamNDI]) -> void:
|
||||
print("NDI sources gone: ", gone_sources.map(func (source: VideoStreamNDI): return source.get_name()))
|
||||
1
addons/godot-ndi/demo/ndi_finder.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://kbgdevvugj7k
|
||||
17
addons/godot-ndi/demo/video_stream_player.gd
Normal file
@@ -0,0 +1,17 @@
|
||||
extends VideoStreamPlayer
|
||||
|
||||
@export var playlist: Array[VideoStream] = []
|
||||
|
||||
func _ready() -> void:
|
||||
_on_finished()
|
||||
|
||||
func _on_finished() -> void:
|
||||
if playlist.is_empty():
|
||||
return
|
||||
|
||||
var next_idx = playlist.find(stream) + 1
|
||||
if next_idx >= playlist.size():
|
||||
next_idx = 0
|
||||
|
||||
stream = playlist[next_idx]
|
||||
play()
|
||||
1
addons/godot-ndi/demo/video_stream_player.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d2sex4jisnkck
|
||||
18
addons/godot-ndi/godot-ndi.gdextension
Normal file
@@ -0,0 +1,18 @@
|
||||
[configuration]
|
||||
|
||||
entry_symbol = "godot_ndi_init"
|
||||
compatibility_minimum = "4.4"
|
||||
reloadable = false
|
||||
|
||||
[libraries]
|
||||
|
||||
linux.x86_64.single = "bin/linux/libgodot-ndi.linux.template_debug.x86_64.so"
|
||||
linux.arm64.single = "bin/linux/libgodot-ndi.linux.template_debug.arm64.so"
|
||||
windows.x86_64.single = "bin/windows/libgodot-ndi.windows.template_debug.x86_64.dll"
|
||||
macos.single = "bin/macos/libgodot-ndi.macos.template_debug.universal.dylib"
|
||||
|
||||
[icons]
|
||||
|
||||
VideoStreamNDI = "icons/VideoStreamNDI.svg"
|
||||
NDIOutput = "icons/NDIOutput.svg"
|
||||
NDIFinder = "icons/NDIFinder.svg"
|
||||
1
addons/godot-ndi/godot-ndi.gdextension.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ccdh6w16n2no5
|
||||
8
addons/godot-ndi/icons/NDIFinder.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path d="m6 10-3.5 3.5" stroke="#e4eded" stroke-linecap="round" stroke-width="3"/>
|
||||
<circle cx="9.3341" cy="6.5" r="4.0547" stroke="#fa3d49" stroke-width="1.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 390 B |
37
addons/godot-ndi/icons/NDIFinder.svg.import
Normal file
@@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bjqtfd5qckc6l"
|
||||
path="res://.godot/imported/NDIFinder.svg-6145b2a9070199a0c93ffe21a332ddf0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot-ndi/icons/NDIFinder.svg"
|
||||
dest_files=["res://.godot/imported/NDIFinder.svg-6145b2a9070199a0c93ffe21a332ddf0.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
|
||||
6
addons/godot-ndi/icons/NDIOutput.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="7.2363" fill="#e4eded" stroke-width="11.339"/>
|
||||
<circle cx="8" cy="8" r="3.247" fill="#fa3d49" stroke-width="1.284"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
37
addons/godot-ndi/icons/NDIOutput.svg.import
Normal file
@@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cvv0gnogt1vac"
|
||||
path="res://.godot/imported/NDIOutput.svg-b19965324d53dea421ada474b6902c23.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot-ndi/icons/NDIOutput.svg"
|
||||
dest_files=["res://.godot/imported/NDIOutput.svg-b19965324d53dea421ada474b6902c23.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
|
||||
7
addons/godot-ndi/icons/VideoStreamNDI.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m11 15 4.3235-7h-4.3235l-3.4993 5.5046-0.08129 1.4954z" fill="#484d3d"/>
|
||||
<path d="m11 15s-6-1-6-7 6-7 6-7h-10v14z" fill="#e4eded"/>
|
||||
<circle cx="11" cy="8" r="2.5288" fill="#fa3d49"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 397 B |
37
addons/godot-ndi/icons/VideoStreamNDI.svg.import
Normal file
@@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://pfqe4htodj7k"
|
||||
path="res://.godot/imported/VideoStreamNDI.svg-79e19fa3a194ede543418d01dcbbb6a7.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot-ndi/icons/VideoStreamNDI.svg"
|
||||
dest_files=["res://.godot/imported/VideoStreamNDI.svg-79e19fa3a194ede543418d01dcbbb6a7.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
|
||||
58
autoload/blend_mode_manager.gd
Normal file
@@ -0,0 +1,58 @@
|
||||
extends Node
|
||||
|
||||
## Blend Mode Manager
|
||||
##
|
||||
## Manages blend mode constants and provides utility functions for
|
||||
## working with blend modes in the PNGTuber application.
|
||||
|
||||
## Blend mode constants
|
||||
enum BlendMode {
|
||||
NORMAL = 0,
|
||||
MULTIPLY = 1,
|
||||
SCREEN = 2,
|
||||
OVERLAY = 3,
|
||||
SOFT_LIGHT = 4,
|
||||
HARD_LIGHT = 5,
|
||||
COLOR_DODGE = 6,
|
||||
COLOR_BURN = 7,
|
||||
DARKEN = 8,
|
||||
LIGHTEN = 9,
|
||||
ADD = 10, # Linear Dodge
|
||||
SUBTRACT = 11
|
||||
}
|
||||
|
||||
## Returns an array of blend mode names for use in dropdowns
|
||||
static func get_blend_mode_names() -> Array:
|
||||
return [
|
||||
"Normal",
|
||||
"Multiply",
|
||||
"Screen",
|
||||
"Overlay",
|
||||
"Soft Light",
|
||||
"Hard Light",
|
||||
"Color Dodge",
|
||||
"Color Burn",
|
||||
"Darken",
|
||||
"Lighten",
|
||||
"Add",
|
||||
"Subtract"
|
||||
]
|
||||
|
||||
## Returns the string name for a given blend mode enum value
|
||||
static func get_blend_mode_name(mode: BlendMode) -> String:
|
||||
var names = get_blend_mode_names()
|
||||
if mode >= 0 and mode < names.size():
|
||||
return names[mode]
|
||||
return "Normal"
|
||||
|
||||
## Returns the blend mode enum value for a given name
|
||||
static func get_blend_mode_from_name(name: String) -> BlendMode:
|
||||
var names = get_blend_mode_names()
|
||||
var index = names.find(name)
|
||||
if index != -1:
|
||||
return index as BlendMode
|
||||
return BlendMode.NORMAL
|
||||
|
||||
## Returns whether a blend mode is valid
|
||||
static func is_valid_blend_mode(mode: int) -> bool:
|
||||
return mode >= BlendMode.NORMAL and mode <= BlendMode.SUBTRACT
|
||||
1
autoload/blend_mode_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bm1aeoarbpp6b
|
||||
@@ -29,13 +29,13 @@ var blink = false
|
||||
var blinkTick = 0
|
||||
|
||||
#Audio Listener
|
||||
|
||||
var currentMicrophone = null
|
||||
var playa:AudioStreamPlayer
|
||||
|
||||
var speaking = false
|
||||
var spectrum
|
||||
var volume = 0
|
||||
var volumeSensitivity = 0.0
|
||||
var experimentalMicLoudness = false
|
||||
|
||||
var volumeLimit = 0.0
|
||||
var senseLimit = 0.0
|
||||
@@ -44,8 +44,6 @@ var senseLimit = 0.0
|
||||
signal startSpeaking
|
||||
signal stopSpeaking
|
||||
|
||||
var micResetTime = 180
|
||||
|
||||
var updatePusherNode = null
|
||||
|
||||
var rand = RandomNumberGenerator.new()
|
||||
@@ -56,28 +54,23 @@ func _ready():
|
||||
if !Saving.settings.has("useStreamDeck"):
|
||||
Saving.settings["useStreamDeck"] = false
|
||||
|
||||
if Saving.settings.has("secondsToMicReset"):
|
||||
Global.micResetTime = Saving.settings["secondsToMicReset"]
|
||||
if Saving.settings.has("experimentalMicLoudness"):
|
||||
Global.experimentalMicLoudness = Saving.settings["experimentalMicLoudness"]
|
||||
else:
|
||||
Saving.settings["secondsToMicReset"] = 180
|
||||
|
||||
Saving.settings["experimentalMicLoudness"] = false
|
||||
|
||||
createMicrophone()
|
||||
|
||||
func createMicrophone():
|
||||
var playa = AudioStreamPlayer.new()
|
||||
var mic = AudioStreamMicrophone.new()
|
||||
playa.stream = mic
|
||||
playa.autoplay = true
|
||||
if playa != null:
|
||||
remove_child(playa)
|
||||
playa.free()
|
||||
playa = AudioStreamPlayer.new()
|
||||
playa.bus = "MIC"
|
||||
playa.stream = AudioStreamMicrophone.new()
|
||||
add_child(playa)
|
||||
currentMicrophone = playa
|
||||
await get_tree().create_timer(micResetTime).timeout
|
||||
if currentMicrophone != playa:
|
||||
return
|
||||
deleteAllMics()
|
||||
currentMicrophone = null
|
||||
await get_tree().create_timer(0.25).timeout
|
||||
createMicrophone()
|
||||
await get_tree().create_timer(0.25).timeout # apparently it works to fix WASAPI "Initialize failed with error 0xffffffff88890002" and "init_input_device" errors
|
||||
playa.play()
|
||||
|
||||
func deleteAllMics():
|
||||
for child in get_children():
|
||||
@@ -87,8 +80,9 @@ func deleteAllMics():
|
||||
func _process(delta):
|
||||
animationTick += 1
|
||||
|
||||
volume = spectrum.get_magnitude_for_frequency_range(20, 20000).length()
|
||||
if currentMicrophone != null:
|
||||
volume = (AudioServer.get_bus_peak_volume_left_db(1, 0) + AudioServer.get_bus_peak_volume_right_db(1, 0)) / 1600.0 + 0.25 if experimentalMicLoudness else spectrum.get_magnitude_for_frequency_range(20, 20000).length()
|
||||
|
||||
if playa != null:
|
||||
volumeSensitivity = lerp(volumeSensitivity,0.0,delta*2)
|
||||
|
||||
if volume>volumeLimit:
|
||||
@@ -112,7 +106,7 @@ func _process(delta):
|
||||
heldSprite.z += 1
|
||||
heldSprite.setZIndex()
|
||||
pushUpdate("Moved sprite layer.")
|
||||
if main.editMode:
|
||||
if main and main.editMode:
|
||||
if Input.is_action_just_pressed("reparent"):
|
||||
reparentMode = !reparentMode
|
||||
Global.chain.enable(reparentMode)
|
||||
@@ -121,7 +115,7 @@ func _process(delta):
|
||||
reparentMode = false
|
||||
Global.chain.enable(reparentMode)
|
||||
|
||||
if main.editMode:
|
||||
if main and main.editMode:
|
||||
if reparentMode:
|
||||
RenderingServer.set_default_clear_color(Color.POWDER_BLUE)
|
||||
else:
|
||||
@@ -132,7 +126,7 @@ func _process(delta):
|
||||
scrollSprites()
|
||||
|
||||
|
||||
if !main.fileSystemOpen:
|
||||
if !main or main.fileSystemOpen:
|
||||
|
||||
if Input.is_action_just_pressed("refresh"):
|
||||
refresh()
|
||||
@@ -228,7 +222,7 @@ func scrollSprites():
|
||||
if Input.is_action_pressed("control"):
|
||||
return
|
||||
|
||||
if !main.editMode:
|
||||
if !main or !main.editMode:
|
||||
return
|
||||
|
||||
if main.fileSystemOpen:
|
||||
@@ -286,7 +280,6 @@ func blinking():
|
||||
blinkTick = -12
|
||||
|
||||
func epicFail(err):
|
||||
print(fail)
|
||||
if fail == null:
|
||||
return
|
||||
|
||||
|
||||
@@ -54,13 +54,13 @@ var settings = {
|
||||
"bounce":250,
|
||||
"gravity":1000,
|
||||
"maxFPS":60,
|
||||
"secondsToMicReset":180,
|
||||
"backgroundColor":var_to_str(Color(0.0,0.0,0.0,0.0)),
|
||||
"filtering":false,
|
||||
"costumeKeys":["1","2","3","4","5","6","7","8","9","0"],
|
||||
"blinkSpeed":1.0,
|
||||
"blinkChance":200,
|
||||
"bounceOnCostumeChange":false,
|
||||
"experimentalMicLoudness":false,
|
||||
}
|
||||
|
||||
var settingsPath = "user://settings.pngtp"
|
||||
|
||||
336
autoload/wobble_sync_manager.gd
Normal file
@@ -0,0 +1,336 @@
|
||||
extends Node
|
||||
|
||||
## WobbleSyncGroupManager - Manages synchronized wobble groups for sprites
|
||||
## Follows class-based OOP pattern as requested
|
||||
|
||||
var syncGroups: Dictionary = {} # groupName: WobbleSyncGroup
|
||||
var spriteGroupMembership: Dictionary = {} # spriteId: groupName
|
||||
|
||||
signal group_created(group_name: String)
|
||||
signal group_deleted(group_name: String)
|
||||
signal sprite_joined_group(sprite_id: int, group_name: String)
|
||||
signal sprite_left_group(sprite_id: int, group_name: String)
|
||||
|
||||
func _ready():
|
||||
# Manager initialization complete
|
||||
pass
|
||||
|
||||
class WobbleSyncGroup:
|
||||
var name: String
|
||||
var xFrq: float = 0.0
|
||||
var xAmp: float = 0.0
|
||||
var yFrq: float = 0.0
|
||||
var yAmp: float = 0.0
|
||||
var rFrq: float = 0.0
|
||||
var rAmp: float = 0.0
|
||||
var memberSprites: Array = []
|
||||
|
||||
func _init(group_name: String):
|
||||
name = group_name
|
||||
|
||||
## Set wobble parameters for the entire group
|
||||
func setWobbleParameters(x_frq: float, x_amp: float, y_frq: float, y_amp: float, r_frq: float, r_amp: float, manager_ref):
|
||||
xFrq = x_frq
|
||||
xAmp = x_amp
|
||||
yFrq = y_frq
|
||||
yAmp = y_amp
|
||||
rFrq = r_frq
|
||||
rAmp = r_amp
|
||||
|
||||
# Update all member sprites using the manager reference
|
||||
var sprites = manager_ref.get_tree().get_nodes_in_group("saved")
|
||||
|
||||
var updated_count = 0
|
||||
for sprite in sprites:
|
||||
if sprite.id in memberSprites:
|
||||
# Preserve the sprite's group membership
|
||||
var current_group = sprite.wobbleSyncGroup
|
||||
sprite.xFrq = xFrq
|
||||
sprite.xAmp = xAmp
|
||||
sprite.yFrq = yFrq
|
||||
sprite.yAmp = yAmp
|
||||
sprite.rFrq = rFrq
|
||||
sprite.rAmp = rAmp
|
||||
# Restore group membership in case it got cleared
|
||||
sprite.wobbleSyncGroup = current_group
|
||||
updated_count += 1
|
||||
|
||||
## Add sprite to this group
|
||||
func addSprite(sprite_id): # Remove type constraint to handle float IDs
|
||||
if sprite_id not in memberSprites:
|
||||
memberSprites.append(sprite_id)
|
||||
else:
|
||||
pass # Sprite already in group
|
||||
|
||||
## Remove sprite from this group
|
||||
func removeSprite(sprite_id): # Remove type constraint
|
||||
memberSprites.erase(sprite_id)
|
||||
|
||||
## Check if group is empty
|
||||
func isEmpty() -> bool:
|
||||
return memberSprites.size() == 0
|
||||
|
||||
## Get group data for saving
|
||||
func toSaveData() -> Dictionary:
|
||||
return {
|
||||
"name": name,
|
||||
"xFrq": xFrq,
|
||||
"xAmp": xAmp,
|
||||
"yFrq": yFrq,
|
||||
"yAmp": yAmp,
|
||||
"rFrq": rFrq,
|
||||
"rAmp": rAmp,
|
||||
"memberSprites": memberSprites
|
||||
}
|
||||
|
||||
## Load group data from save
|
||||
func fromSaveData(data: Dictionary):
|
||||
name = data.get("name", "")
|
||||
xFrq = data.get("xFrq", 0.0)
|
||||
xAmp = data.get("xAmp", 0.0)
|
||||
yFrq = data.get("yFrq", 0.0)
|
||||
yAmp = data.get("yAmp", 0.0)
|
||||
rFrq = data.get("rFrq", 0.0)
|
||||
rAmp = data.get("rAmp", 0.0)
|
||||
memberSprites = data.get("memberSprites", [])
|
||||
|
||||
## Create a new wobble sync group
|
||||
func createGroup(group_name: String, sprite_id = -1) -> bool: # Remove type constraint
|
||||
if group_name.strip_edges() == "" or syncGroups.has(group_name):
|
||||
return false
|
||||
|
||||
var group = WobbleSyncGroup.new(group_name)
|
||||
syncGroups[group_name] = group
|
||||
|
||||
# If a sprite ID is provided, add it and copy its wobble values
|
||||
if sprite_id != -1:
|
||||
var sprite = getSpriteById(sprite_id)
|
||||
if sprite:
|
||||
# Store the sprite's raw wobble values directly
|
||||
group.setWobbleParameters(sprite.xFrq, sprite.xAmp, sprite.yFrq, sprite.yAmp, sprite.rFrq, sprite.rAmp, self)
|
||||
addSpriteToGroup(sprite_id, group_name)
|
||||
|
||||
group_created.emit(group_name)
|
||||
Global.pushUpdate("Created wobble sync group: " + group_name)
|
||||
return true
|
||||
|
||||
## Delete a wobble sync group
|
||||
func deleteGroup(group_name: String):
|
||||
if not syncGroups.has(group_name):
|
||||
return
|
||||
|
||||
var group = syncGroups[group_name]
|
||||
|
||||
# Remove all sprites from the group
|
||||
for sprite_id in group.memberSprites.duplicate():
|
||||
removeSpriteFromGroup(sprite_id, group_name)
|
||||
|
||||
syncGroups.erase(group_name)
|
||||
group_deleted.emit(group_name)
|
||||
Global.pushUpdate("Deleted wobble sync group: " + group_name)
|
||||
|
||||
## Add sprite to a group
|
||||
func addSpriteToGroup(sprite_id, group_name: String) -> bool: # Remove type constraint
|
||||
|
||||
if not syncGroups.has(group_name):
|
||||
return false
|
||||
|
||||
# Remove sprite from any existing group first
|
||||
removeSpriteFromAllGroups(sprite_id)
|
||||
|
||||
var group = syncGroups[group_name]
|
||||
group.addSprite(sprite_id)
|
||||
spriteGroupMembership[sprite_id] = group_name
|
||||
|
||||
# Sync the sprite's wobble parameters to the group
|
||||
var sprite = getSpriteById(sprite_id)
|
||||
if sprite:
|
||||
sprite.wobbleSyncGroup = group_name
|
||||
sprite.xFrq = group.xFrq
|
||||
sprite.xAmp = group.xAmp
|
||||
sprite.yFrq = group.yFrq
|
||||
sprite.yAmp = group.yAmp
|
||||
sprite.rFrq = group.rFrq
|
||||
sprite.rAmp = group.rAmp
|
||||
|
||||
sprite_joined_group.emit(sprite_id, group_name)
|
||||
Global.pushUpdate("Added sprite to wobble sync group: " + group_name)
|
||||
return true
|
||||
|
||||
## Remove sprite from a specific group
|
||||
func removeSpriteFromGroup(sprite_id, group_name: String): # Remove type constraint
|
||||
if not syncGroups.has(group_name):
|
||||
return
|
||||
|
||||
var group = syncGroups[group_name]
|
||||
group.removeSprite(sprite_id)
|
||||
spriteGroupMembership.erase(sprite_id)
|
||||
|
||||
# Clear the sprite's sync group reference
|
||||
var sprite = getSpriteById(sprite_id)
|
||||
if sprite:
|
||||
sprite.wobbleSyncGroup = ""
|
||||
else:
|
||||
pass # Sprite not found
|
||||
|
||||
sprite_left_group.emit(sprite_id, group_name)
|
||||
|
||||
# Auto-delete empty groups
|
||||
if group.isEmpty():
|
||||
deleteGroup(group_name)
|
||||
else:
|
||||
Global.pushUpdate("Removed sprite from wobble sync group: " + group_name)
|
||||
|
||||
## Remove sprite from all groups
|
||||
func removeSpriteFromAllGroups(sprite_id): # Remove type constraint
|
||||
if spriteGroupMembership.has(sprite_id):
|
||||
var group_name = spriteGroupMembership[sprite_id]
|
||||
removeSpriteFromGroup(sprite_id, group_name)
|
||||
else:
|
||||
pass # Sprite not in any group
|
||||
|
||||
## Update wobble parameters for a group
|
||||
func updateGroupWobble(group_name: String, x_frq: float, x_amp: float, y_frq: float, y_amp: float, r_frq: float, r_amp: float):
|
||||
|
||||
if not syncGroups.has(group_name):
|
||||
return
|
||||
|
||||
var group = syncGroups[group_name]
|
||||
group.setWobbleParameters(x_frq, x_amp, y_frq, y_amp, r_frq, r_amp, self)
|
||||
|
||||
## Get sprite's current group name
|
||||
func getSpriteGroupName(sprite_id: int) -> String:
|
||||
return spriteGroupMembership.get(sprite_id, "")
|
||||
|
||||
## Get all group names
|
||||
func getGroupNames() -> Array:
|
||||
return syncGroups.keys()
|
||||
|
||||
## Test function to manually load wobble sync groups
|
||||
func testManualLoad():
|
||||
var test_data = {
|
||||
"ears": {
|
||||
"name": "ears",
|
||||
"xFrq": 0.0,
|
||||
"xAmp": 0.0,
|
||||
"yFrq": 0.019,
|
||||
"yAmp": 3.0,
|
||||
"rFrq": 0.399,
|
||||
"rAmp": 47.0,
|
||||
"memberSprites": [3291286625.0]
|
||||
}
|
||||
}
|
||||
await loadSaveData(test_data)
|
||||
|
||||
## Check if a group exists
|
||||
func hasGroup(group_name: String) -> bool:
|
||||
return syncGroups.has(group_name)
|
||||
|
||||
## Get sprite by ID
|
||||
func getSpriteById(sprite_id): # Remove type constraint to handle float IDs
|
||||
var sprites = get_tree().get_nodes_in_group("saved")
|
||||
for sprite in sprites:
|
||||
if sprite.id == sprite_id:
|
||||
return sprite
|
||||
return null
|
||||
|
||||
## Clean up orphaned sprites (sprites that no longer exist)
|
||||
func cleanupOrphanedSprites():
|
||||
var existing_sprite_ids = []
|
||||
var sprites = get_tree().get_nodes_in_group("saved")
|
||||
for sprite in sprites:
|
||||
existing_sprite_ids.append(sprite.id)
|
||||
|
||||
# Check each group for orphaned sprite IDs
|
||||
for group_name in syncGroups.keys():
|
||||
var group = syncGroups[group_name]
|
||||
var orphaned_ids = []
|
||||
|
||||
for sprite_id in group.memberSprites:
|
||||
if sprite_id not in existing_sprite_ids:
|
||||
orphaned_ids.append(sprite_id)
|
||||
|
||||
# Remove orphaned IDs
|
||||
for orphaned_id in orphaned_ids:
|
||||
group.removeSprite(orphaned_id)
|
||||
spriteGroupMembership.erase(orphaned_id)
|
||||
|
||||
# Delete empty groups
|
||||
if group.isEmpty():
|
||||
deleteGroup(group_name)
|
||||
|
||||
## Get save data for all groups
|
||||
func getSaveData() -> Dictionary:
|
||||
var save_data = {}
|
||||
for group_name in syncGroups.keys():
|
||||
var group = syncGroups[group_name]
|
||||
save_data[group_name] = group.toSaveData()
|
||||
return save_data
|
||||
|
||||
## Load save data for all groups
|
||||
func loadSaveData(save_data: Dictionary):
|
||||
|
||||
# Check if save_data is valid
|
||||
if save_data == null or typeof(save_data) != TYPE_DICTIONARY:
|
||||
return
|
||||
|
||||
if save_data.size() == 0:
|
||||
return
|
||||
|
||||
# Clear existing groups
|
||||
syncGroups.clear()
|
||||
spriteGroupMembership.clear()
|
||||
|
||||
# Load groups from save data
|
||||
var loaded_groups = 0
|
||||
for group_name in save_data.keys():
|
||||
var group_data = save_data[group_name]
|
||||
|
||||
if typeof(group_data) == TYPE_DICTIONARY:
|
||||
var group = WobbleSyncGroup.new(group_name)
|
||||
group.fromSaveData(group_data)
|
||||
syncGroups[group_name] = group
|
||||
loaded_groups += 1
|
||||
|
||||
# Update sprite group membership
|
||||
for sprite_id in group.memberSprites:
|
||||
spriteGroupMembership[sprite_id] = group_name
|
||||
else:
|
||||
pass # Invalid group data
|
||||
|
||||
|
||||
# If no groups were loaded, clear any orphaned group references on sprites
|
||||
if syncGroups.size() == 0:
|
||||
var sprites = get_tree().get_nodes_in_group("saved")
|
||||
for sprite in sprites:
|
||||
if sprite.wobbleSyncGroup != "":
|
||||
sprite.wobbleSyncGroup = ""
|
||||
else:
|
||||
# Apply wobble parameters to sprites with better timing
|
||||
await get_tree().process_frame # Wait for sprites to be loaded
|
||||
await get_tree().process_frame # Extra frame to ensure everything is ready
|
||||
|
||||
for group_name in syncGroups.keys():
|
||||
var group = syncGroups[group_name]
|
||||
for sprite_id in group.memberSprites:
|
||||
var sprite = getSpriteById(sprite_id)
|
||||
if sprite:
|
||||
sprite.wobbleSyncGroup = group_name
|
||||
sprite.xFrq = group.xFrq
|
||||
sprite.xAmp = group.xAmp
|
||||
sprite.yFrq = group.yFrq
|
||||
sprite.yAmp = group.yAmp
|
||||
sprite.rFrq = group.rFrq
|
||||
sprite.rAmp = group.rAmp
|
||||
# Force a complete opacity update after modifying sprite properties
|
||||
if sprite.has_method("updateOpacity"):
|
||||
sprite.updateOpacity()
|
||||
if sprite.has_method("updateShaderOpacity"):
|
||||
sprite.updateShaderOpacity()
|
||||
else:
|
||||
pass # Sprite not found
|
||||
|
||||
|
||||
## Called when a sprite is deleted to clean up references
|
||||
func onSpriteDeleted(sprite_id): # Remove type constraint
|
||||
removeSpriteFromAllGroups(sprite_id)
|
||||
1
autoload/wobble_sync_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ccl0r2eov68sy
|
||||
@@ -4,7 +4,8 @@ extends Node2D
|
||||
@onready var editButton = $Edit/Button
|
||||
|
||||
func _process(delta):
|
||||
var uiAnimSpeed = 8.0 # Fixed UI animation speed
|
||||
if Rect2(editButton.get_parent().position-Vector2(24,24),editButton.size).has_point(get_local_mouse_position()):
|
||||
editSprite.scale = lerp(editSprite.scale,Vector2(1.2,1.2),0.2)
|
||||
editSprite.scale = lerp(editSprite.scale, Vector2(1.2,1.2), delta * uiAnimSpeed)
|
||||
else:
|
||||
editSprite.scale = lerp(editSprite.scale,Vector2(1.0,1.0),0.2)
|
||||
editSprite.scale = lerp(editSprite.scale, Vector2(1.0,1.0), delta * uiAnimSpeed)
|
||||
|
||||
@@ -30,7 +30,8 @@ func _process(delta):
|
||||
if buttons[b] == null:
|
||||
continue
|
||||
if Rect2(buttons[b].get_parent().position-Vector2(24,24),buttons[b].size).has_point(get_local_mouse_position()):
|
||||
sprites[s].scale = lerp(sprites[s].scale,Vector2(1.2,1.2),0.2)
|
||||
var uiAnimSpeed = 8.0 # Fixed UI animation speed
|
||||
sprites[s].scale = lerp(sprites[s].scale, Vector2(1.2,1.2), delta * uiAnimSpeed)
|
||||
if Input.is_action_pressed("mouse_left"):
|
||||
sprites[s].scale = Vector2(0.6,0.6)
|
||||
match b:
|
||||
@@ -50,7 +51,8 @@ func _process(delta):
|
||||
Global.mouse.text = "Duplicate sprite"
|
||||
|
||||
else:
|
||||
sprites[s].scale = lerp(sprites[s].scale,Vector2(1.0,1.0),0.2)
|
||||
var uiAnimSpeed = 8.0 # Fixed UI animation speed
|
||||
sprites[s].scale = lerp(sprites[s].scale, Vector2(1.0,1.0), delta * uiAnimSpeed)
|
||||
s += 1
|
||||
|
||||
var newColor = Color.DARK_SLATE_GRAY if Global.heldSprite == null else Color.WHITE
|
||||
|
||||
144
main_scenes/NDIAvatarViewport.gd
Normal file
@@ -0,0 +1,144 @@
|
||||
extends SubViewport
|
||||
|
||||
## NDI Avatar Viewport Controller
|
||||
## Handles avatar-only NDI streaming with synchronized states
|
||||
|
||||
#Node Reference
|
||||
@onready var origin = $OriginMotion/Origin
|
||||
@onready var ndi_output = $NDIOutput
|
||||
|
||||
## Initialize viewport settings for transparent background and avatar mirroring
|
||||
func _ready():
|
||||
# Set transparent background
|
||||
transparent_bg = true
|
||||
|
||||
# Connect to the main scene for state sync
|
||||
if Global.main:
|
||||
Global.main.connect("spriteVisToggles", Callable(self, "_on_sprite_visibility_toggle"))
|
||||
Global.main.connect("bounceChange", Callable(self, "_sync_bounce"))
|
||||
|
||||
## Sync avatar sprites from main scene to this viewport
|
||||
func sync_avatar():
|
||||
# Clear existing sprites
|
||||
for child in origin.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Wait for cleanup
|
||||
await get_tree().process_frame
|
||||
|
||||
# Get sprites from main scene
|
||||
var main_sprites = get_tree().get_nodes_in_group("saved")
|
||||
for sprite in main_sprites:
|
||||
clone_sprite(sprite)
|
||||
|
||||
## Clone a sprite from main scene to this viewport
|
||||
func clone_sprite(original_sprite):
|
||||
# Create new instance
|
||||
var sprite_scene = preload("res://ui_scenes/selectedSprite/spriteObject.tscn")
|
||||
var cloned_sprite = sprite_scene.instantiate()
|
||||
|
||||
# Copy all properties
|
||||
cloned_sprite.path = original_sprite.path
|
||||
cloned_sprite.id = original_sprite.id
|
||||
cloned_sprite.parentId = original_sprite.parentId
|
||||
cloned_sprite.displayName = original_sprite.displayName
|
||||
|
||||
# Visual properties
|
||||
cloned_sprite.offset = original_sprite.offset
|
||||
cloned_sprite.z = original_sprite.z
|
||||
cloned_sprite.position = original_sprite.position
|
||||
|
||||
# Physics properties
|
||||
cloned_sprite.dragSpeed = original_sprite.dragSpeed
|
||||
cloned_sprite.xFrq = original_sprite.xFrq
|
||||
cloned_sprite.xAmp = original_sprite.xAmp
|
||||
cloned_sprite.yFrq = original_sprite.yFrq
|
||||
cloned_sprite.yAmp = original_sprite.yAmp
|
||||
cloned_sprite.rFrq = original_sprite.rFrq
|
||||
cloned_sprite.rAmp = original_sprite.rAmp
|
||||
cloned_sprite.rdragStr = original_sprite.rdragStr
|
||||
cloned_sprite.rLimitMin = original_sprite.rLimitMin
|
||||
cloned_sprite.rLimitMax = original_sprite.rLimitMax
|
||||
cloned_sprite.stretchAmount = original_sprite.stretchAmount
|
||||
cloned_sprite.ignoreBounce = original_sprite.ignoreBounce
|
||||
|
||||
# Animation properties
|
||||
cloned_sprite.frames = original_sprite.frames
|
||||
cloned_sprite.animSpeed = original_sprite.animSpeed
|
||||
|
||||
# Visibility properties
|
||||
cloned_sprite.showOnTalk = original_sprite.showOnTalk
|
||||
cloned_sprite.showOnBlink = original_sprite.showOnBlink
|
||||
cloned_sprite.costumeLayers = original_sprite.costumeLayers.duplicate()
|
||||
cloned_sprite.spriteOpacity = original_sprite.spriteOpacity
|
||||
cloned_sprite.affectChildrenOpacity = original_sprite.affectChildrenOpacity
|
||||
cloned_sprite.blendMode = original_sprite.blendMode
|
||||
cloned_sprite.toggle = original_sprite.toggle
|
||||
cloned_sprite.clipped = original_sprite.clipped
|
||||
|
||||
# Transform properties
|
||||
cloned_sprite.staticRotation = original_sprite.staticRotation
|
||||
cloned_sprite.mirrorHorizontal = original_sprite.mirrorHorizontal
|
||||
cloned_sprite.mirrorVertical = original_sprite.mirrorVertical
|
||||
|
||||
# Wobble sync
|
||||
cloned_sprite.wobbleSyncGroup = original_sprite.wobbleSyncGroup
|
||||
|
||||
# Add to origin
|
||||
origin.add_child(cloned_sprite)
|
||||
|
||||
# Apply current costume visibility
|
||||
if Global.main:
|
||||
var current_costume = Global.main.costume
|
||||
if cloned_sprite.costumeLayers[current_costume - 1] == 1:
|
||||
cloned_sprite.visible = true
|
||||
else:
|
||||
cloned_sprite.visible = false
|
||||
|
||||
## Process function to keep sprites in sync with main scene
|
||||
func _process(delta):
|
||||
if !Global.main:
|
||||
return
|
||||
|
||||
# Sync origin position with bounce from main scene
|
||||
$OriginMotion.position.y = 360 + Global.main.bounceChange
|
||||
|
||||
# Sync sprites with main scene
|
||||
var main_sprites = get_tree().get_nodes_in_group("saved")
|
||||
var avatar_sprites = origin.get_children()
|
||||
|
||||
for i in range(min(main_sprites.size(), avatar_sprites.size())):
|
||||
sync_sprite_state(main_sprites[i], avatar_sprites[i])
|
||||
|
||||
## Sync individual sprite state
|
||||
func sync_sprite_state(main_sprite, avatar_sprite):
|
||||
if !is_instance_valid(main_sprite) or !is_instance_valid(avatar_sprite):
|
||||
return
|
||||
|
||||
# Sync position and transform
|
||||
avatar_sprite.position = main_sprite.position
|
||||
avatar_sprite.rotation = main_sprite.rotation
|
||||
avatar_sprite.scale = main_sprite.scale
|
||||
|
||||
# Sync visibility based on costume
|
||||
if Global.main:
|
||||
var current_costume = Global.main.costume
|
||||
if avatar_sprite.costumeLayers[current_costume - 1] == 1:
|
||||
avatar_sprite.visible = main_sprite.visible
|
||||
else:
|
||||
avatar_sprite.visible = false
|
||||
|
||||
# Sync animation frame
|
||||
if avatar_sprite.has_method("get_sprite") and main_sprite.has_method("get_sprite"):
|
||||
var avatar_spr = avatar_sprite.get_sprite()
|
||||
var main_spr = main_sprite.get_sprite()
|
||||
if avatar_spr and main_spr:
|
||||
avatar_spr.frame = main_spr.frame
|
||||
|
||||
## Handle sprite visibility toggles
|
||||
func _on_sprite_visibility_toggle():
|
||||
sync_avatar()
|
||||
|
||||
## Sync bounce changes
|
||||
func _sync_bounce(bounce_change):
|
||||
$OriginMotion.position.y = 360 + bounce_change
|
||||
1
main_scenes/NDIAvatarViewport.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dkomsdn2pvjwp
|
||||
18
main_scenes/NDIAvatarViewport.tscn
Normal file
@@ -0,0 +1,18 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://main_scenes/NDIAvatarViewport.gd" id="1"]
|
||||
|
||||
[node name="SubViewport" type="SubViewport"]
|
||||
handle_input_locally = false
|
||||
size = Vector2i(1280, 720)
|
||||
render_target_update_mode = 4
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="NDIOutput" type="NDIOutput"]
|
||||
name = "PNGTuber Avatar"
|
||||
enable_editor_output = true
|
||||
|
||||
[node name="OriginMotion" type="Node2D"]
|
||||
position = Vector2(640, 360)
|
||||
|
||||
[node name="Origin" type="Node2D" parent="OriginMotion"]
|
||||
110
main_scenes/NDIOutputManager.gd
Normal file
@@ -0,0 +1,110 @@
|
||||
class_name NDIOutputManager
|
||||
extends Node
|
||||
|
||||
## NDI Output Manager for PNGTuber-Plus
|
||||
## Handles NDI streaming output for OBS integration
|
||||
##
|
||||
## This class manages the NDI output functionality, allowing the PNGTuber-Plus
|
||||
## avatar to be streamed as an NDI source that can be captured by OBS Studio
|
||||
## or other NDI-compatible applications.
|
||||
|
||||
@export var ndi_source_name: String = "PNGTuber-Plus"
|
||||
@export var enable_audio: bool = true
|
||||
@export var audio_bus_name: String = "Master"
|
||||
@export var auto_start: bool = true
|
||||
|
||||
var ndi_output: NDIOutput
|
||||
var is_streaming: bool = false
|
||||
|
||||
## Called when the node enters the scene tree
|
||||
func _ready() -> void:
|
||||
if auto_start:
|
||||
initialize_ndi_output()
|
||||
|
||||
## Initialize the NDI output node and configure it
|
||||
func initialize_ndi_output() -> void:
|
||||
# Create NDI output node if it doesn't exist
|
||||
if not ndi_output:
|
||||
ndi_output = NDIOutput.new()
|
||||
add_child(ndi_output)
|
||||
|
||||
# Configure NDI output settings
|
||||
configure_ndi_output()
|
||||
|
||||
print("NDI Output initialized with source name: ", ndi_source_name)
|
||||
|
||||
## Configure the NDI output with current settings
|
||||
func configure_ndi_output() -> void:
|
||||
if not ndi_output:
|
||||
push_error("NDI Output node not found")
|
||||
return
|
||||
|
||||
# Set the NDI source name
|
||||
ndi_output.name = ndi_source_name
|
||||
|
||||
# Configure audio if enabled
|
||||
if enable_audio:
|
||||
ndi_output.audio_bus = StringName(audio_bus_name)
|
||||
|
||||
print("NDI Output configured - Name: ", ndi_source_name, " Audio: ", enable_audio)
|
||||
|
||||
## Start NDI streaming
|
||||
func start_streaming() -> void:
|
||||
if not ndi_output:
|
||||
initialize_ndi_output()
|
||||
|
||||
if ndi_output and not is_streaming:
|
||||
is_streaming = true
|
||||
print("NDI streaming started - Source: ", ndi_source_name)
|
||||
|
||||
## Stop NDI streaming
|
||||
func stop_streaming() -> void:
|
||||
if ndi_output and is_streaming:
|
||||
is_streaming = false
|
||||
print("NDI streaming stopped")
|
||||
|
||||
## Toggle NDI streaming on/off
|
||||
func toggle_streaming() -> bool:
|
||||
if is_streaming:
|
||||
stop_streaming()
|
||||
return false
|
||||
else:
|
||||
start_streaming()
|
||||
return true
|
||||
|
||||
## Update the NDI source name
|
||||
func set_source_name(new_name: String) -> void:
|
||||
ndi_source_name = new_name
|
||||
if ndi_output:
|
||||
ndi_output.name = ndi_source_name
|
||||
print("NDI source name changed to: ", ndi_source_name)
|
||||
|
||||
## Enable or disable audio streaming
|
||||
func set_audio_enabled(enabled: bool) -> void:
|
||||
enable_audio = enabled
|
||||
if ndi_output:
|
||||
if enable_audio:
|
||||
ndi_output.audio_bus = StringName(audio_bus_name)
|
||||
else:
|
||||
ndi_output.audio_bus = StringName("")
|
||||
print("NDI audio ", "enabled" if enabled else "disabled")
|
||||
|
||||
## Change the audio bus for NDI output
|
||||
func set_audio_bus(bus_name: String) -> void:
|
||||
audio_bus_name = bus_name
|
||||
if ndi_output and enable_audio:
|
||||
ndi_output.audio_bus = StringName(audio_bus_name)
|
||||
print("NDI audio bus changed to: ", audio_bus_name)
|
||||
|
||||
## Get current streaming status
|
||||
func get_streaming_status() -> bool:
|
||||
return is_streaming
|
||||
|
||||
## Get current NDI source name
|
||||
func get_source_name() -> String:
|
||||
return ndi_source_name
|
||||
|
||||
## Clean up when node is removed
|
||||
func _exit_tree() -> void:
|
||||
if ndi_output and is_streaming:
|
||||
stop_streaming()
|
||||
1
main_scenes/NDIOutputManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c1wo8w4ac848b
|
||||
@@ -8,7 +8,7 @@ var editMode = true
|
||||
@onready var controlPanel = $ControlPanel
|
||||
@onready var editControls = $EditControls
|
||||
@onready var tutorial = $Tutorial
|
||||
@onready var spriteViewer = $EditControls/SpriteViewer
|
||||
@onready var spriteViewer = $EditControls/Container/SpriteViewer
|
||||
@onready var viewerArrows = $ViewerArrows
|
||||
@onready var spriteList = $EditControls/SpriteList
|
||||
|
||||
@@ -25,6 +25,9 @@ var editMode = true
|
||||
|
||||
@onready var shadow = $shadowSprite
|
||||
|
||||
@onready var ndi_output = $NDIOutput
|
||||
@onready var ndi_avatar_viewport = $NDIAvatarViewport
|
||||
|
||||
|
||||
#Scene Reference
|
||||
@onready var spriteObject = preload("res://ui_scenes/selectedSprite/spriteObject.tscn")
|
||||
@@ -36,6 +39,10 @@ var yVel = 0
|
||||
var bounceSlider = 250
|
||||
var bounceGravity = 1000
|
||||
|
||||
# Physics timing multiplier - adjust this to fine-tune physics speed
|
||||
# 1.0 = very slow, 10.0 = reasonable, 60.0 = old frame-based speed
|
||||
var physicsTimeMultiplier = 6.0
|
||||
|
||||
#Costumes
|
||||
var costume = 1
|
||||
var bounceOnCostumeChange = false
|
||||
@@ -43,6 +50,18 @@ var bounceOnCostumeChange = false
|
||||
#Zooming
|
||||
var scaleOverall = 100
|
||||
|
||||
#Camera Panning
|
||||
var isPanning = false
|
||||
var initialPanPosition = Vector2.ZERO
|
||||
var initialCameraOffset = Vector2.ZERO
|
||||
var cameraOffset = Vector2.ZERO
|
||||
|
||||
#View Mode Panning
|
||||
var viewModePanning = false
|
||||
var initialViewPanPosition = Vector2.ZERO
|
||||
var viewPanOffset = Vector2.ZERO
|
||||
var viewPanLerpSpeed = 3.0
|
||||
|
||||
var bounceChange = 0.0
|
||||
|
||||
#IMPORTANT
|
||||
@@ -59,17 +78,23 @@ func _ready():
|
||||
Global.main = self
|
||||
Global.fail = $Failed
|
||||
|
||||
var newUser = Saving.settings["newUser"]
|
||||
|
||||
Global.connect("startSpeaking",onSpeak)
|
||||
|
||||
ElgatoStreamDeck.on_key_down.connect(changeCostumeStreamDeck)
|
||||
# Connect to StreamDeck if available
|
||||
var streamdeck_node = get_node_or_null("/root/ElgatoStreamDeck")
|
||||
if streamdeck_node != null:
|
||||
streamdeck_node.on_key_down.connect(changeCostumeStreamDeck)
|
||||
|
||||
if Saving.settings["newUser"]:
|
||||
_on_load_dialog_file_selected("default")
|
||||
Saving.settings["newUser"] = false
|
||||
saveLoaded = true
|
||||
if newUser:
|
||||
loadDefaultAvatar()
|
||||
else:
|
||||
_on_load_dialog_file_selected(Saving.settings["lastAvatar"])
|
||||
var lastAvatar = Saving.settings.get("lastAvatar", "")
|
||||
if lastAvatar != null and lastAvatar != "" and typeof(lastAvatar) == TYPE_STRING and FileAccess.file_exists(lastAvatar):
|
||||
loadAvatar(lastAvatar)
|
||||
else:
|
||||
loadDefaultAvatar()
|
||||
|
||||
$ControlPanel/volumeSlider.value = Saving.settings["volume"]
|
||||
$ControlPanel/sensitiveSlider.value = Saving.settings["sense"]
|
||||
@@ -130,40 +155,130 @@ func _ready():
|
||||
|
||||
var s = get_viewport().get_visible_rect().size
|
||||
origin.position = s*0.5
|
||||
camera.position = origin.position
|
||||
updateCameraPosition()
|
||||
|
||||
# Initialize avatar viewport
|
||||
if ndi_avatar_viewport:
|
||||
ndi_avatar_viewport.visible = false # Start disabled
|
||||
|
||||
# Test NDI output on startup
|
||||
call_deferred("toggle_ndi")
|
||||
|
||||
func _process(delta):
|
||||
var hold = origin.get_parent().position.y
|
||||
|
||||
origin.get_parent().position.y += yVel * 0.0166
|
||||
origin.get_parent().position.y += yVel * delta * physicsTimeMultiplier
|
||||
if origin.get_parent().position.y > 0:
|
||||
origin.get_parent().position.y = 0
|
||||
bounceChange = hold - origin.get_parent().position.y
|
||||
|
||||
yVel += bounceGravity*0.0166
|
||||
yVel += bounceGravity * delta * physicsTimeMultiplier
|
||||
|
||||
# Handle view mode panning lerp back to center
|
||||
if !editMode:
|
||||
if !viewModePanning:
|
||||
# Lerp back to center when not actively panning
|
||||
viewPanOffset = viewPanOffset.lerp(Vector2.ZERO, viewPanLerpSpeed * delta)
|
||||
|
||||
# Apply view pan offset to the root character (affects physics)
|
||||
origin.position = (get_viewport().get_visible_rect().size * 0.5) + viewPanOffset
|
||||
updateCameraPosition()
|
||||
|
||||
if Input.is_action_just_pressed("openFolder"):
|
||||
OS.shell_open(ProjectSettings.globalize_path("user://"))
|
||||
|
||||
moveSpriteMenu(delta)
|
||||
zoomScene()
|
||||
zoomScene(delta)
|
||||
|
||||
fileSystemOpen = isFileSystemOpen()
|
||||
|
||||
followShadow()
|
||||
|
||||
func followShadow():
|
||||
shadow.visible = is_instance_valid(Global.heldSprite)
|
||||
if !shadow.visible:
|
||||
return
|
||||
## Handle input events, especially for camera panning
|
||||
func _input(event):
|
||||
# Handle middle mouse button for camera panning/reset
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||
if editMode:
|
||||
if event.pressed:
|
||||
# Start panning in edit mode
|
||||
isPanning = true
|
||||
initialPanPosition = event.global_position
|
||||
initialCameraOffset = cameraOffset
|
||||
else:
|
||||
# Stop panning
|
||||
isPanning = false
|
||||
else:
|
||||
if event.pressed:
|
||||
# Start view mode panning
|
||||
viewModePanning = true
|
||||
initialViewPanPosition = event.global_position
|
||||
else:
|
||||
# Stop view mode panning
|
||||
viewModePanning = false
|
||||
|
||||
shadow.global_position = Global.heldSprite.sprite.global_position + Vector2(6,6)
|
||||
shadow.global_rotation = Global.heldSprite.sprite.global_rotation
|
||||
shadow.offset = Global.heldSprite.sprite.offset
|
||||
# Handle mouse movement during edit mode panning
|
||||
elif editMode and event is InputEventMouseMotion and isPanning:
|
||||
var totalDelta = event.global_position - initialPanPosition
|
||||
|
||||
shadow.texture = Global.heldSprite.sprite.texture
|
||||
shadow.hframes = Global.heldSprite.sprite.hframes
|
||||
shadow.frame = Global.heldSprite.sprite.frame
|
||||
# Scale pan distance based on zoom level for more intuitive panning
|
||||
# Higher zoom (closer) = less movement, lower zoom (farther) = more movement
|
||||
var zoomScale = 1.0 / camera.zoom.x
|
||||
var scaledDelta = totalDelta * zoomScale
|
||||
|
||||
# Calculate new camera offset from initial position plus scaled movement
|
||||
cameraOffset = initialCameraOffset - scaledDelta
|
||||
|
||||
# Update camera position immediately
|
||||
var s = get_viewport().get_visible_rect().size
|
||||
camera.position = origin.position + cameraOffset
|
||||
|
||||
# Update UI positions
|
||||
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
||||
tutorial.position = controlPanel.position
|
||||
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
||||
viewerArrows.position = editControls.position
|
||||
pushUpdates.position.y = controlPanel.position.y
|
||||
pushUpdates.position.x = editControls.position.x
|
||||
|
||||
# Handle mouse movement during view mode panning
|
||||
elif !editMode and event is InputEventMouseMotion and viewModePanning:
|
||||
var totalDelta = event.global_position - initialViewPanPosition
|
||||
|
||||
# Scale pan distance for view mode (less sensitive than edit mode)
|
||||
# Invert the movement so dragging right moves character right (not left)
|
||||
var panSensitivity = 0.5
|
||||
viewPanOffset = -totalDelta * panSensitivity
|
||||
|
||||
# Handle NDI output hotkeys
|
||||
elif event is InputEventKey and event.pressed:
|
||||
# F9 key to toggle NDI streaming
|
||||
if event.keycode == KEY_F9:
|
||||
toggle_ndi_streaming()
|
||||
# F10 key to restart NDI streaming with new settings
|
||||
elif event.keycode == KEY_F10:
|
||||
restart_ndi_streaming()
|
||||
|
||||
## Toggle NDI streaming on/off
|
||||
func toggle_ndi_streaming():
|
||||
if ndi_output:
|
||||
print("NDI Output is available - Source: ", ndi_output.name)
|
||||
Global.pushUpdate("NDI Output available - Source: " + ndi_output.name)
|
||||
else:
|
||||
print("NDI Output not found")
|
||||
Global.pushUpdate("NDI Output not found")
|
||||
|
||||
## Restart NDI streaming to apply any new settings
|
||||
func restart_ndi_streaming():
|
||||
if ndi_output:
|
||||
print("NDI Output available - Source: ", ndi_output.name)
|
||||
Global.pushUpdate("NDI Output available - Source: " + ndi_output.name)
|
||||
else:
|
||||
print("NDI Output not found")
|
||||
Global.pushUpdate("NDI Output not found")
|
||||
|
||||
func followShadow():
|
||||
# Shadow disabled for selected sprites
|
||||
shadow.visible = false
|
||||
|
||||
|
||||
func isFileSystemOpen():
|
||||
@@ -173,6 +288,11 @@ func isFileSystemOpen():
|
||||
return true
|
||||
Global.heldSprite = null
|
||||
return true
|
||||
|
||||
# Check wobble sync group dialog
|
||||
if Global.spriteEdit and Global.spriteEdit.wobbleSyncControl and Global.spriteEdit.wobbleSyncControl.createGroupDialog and Global.spriteEdit.wobbleSyncControl.createGroupDialog.visible:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
#Displays control panel whether or not application is focused
|
||||
@@ -198,16 +318,10 @@ func onWindowSizeChange():
|
||||
lines.position = s*0.5
|
||||
lines.drawLine()
|
||||
|
||||
camera.position = origin.position
|
||||
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
||||
tutorial.position = controlPanel.position
|
||||
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
||||
viewerArrows.position = editControls.position
|
||||
updateCameraPosition()
|
||||
spriteList.position.x = s.x - 233
|
||||
pushUpdates.position.y = controlPanel.position.y
|
||||
pushUpdates.position.x = editControls.position.x
|
||||
|
||||
func zoomScene():
|
||||
func zoomScene(delta):
|
||||
#Handles Zooming
|
||||
if Input.is_action_pressed("control"):
|
||||
if Input.is_action_just_pressed("scrollUp"):
|
||||
@@ -221,7 +335,7 @@ func zoomScene():
|
||||
scaleOverall -= 10
|
||||
changeZoom()
|
||||
|
||||
$ControlPanel/ZoomLabel.modulate.a = lerp($ControlPanel/ZoomLabel.modulate.a,0.0,0.02)
|
||||
$ControlPanel/ZoomLabel.modulate.a = lerp($ControlPanel/ZoomLabel.modulate.a, 0.0, delta * (physicsTimeMultiplier * 1.2))
|
||||
|
||||
func changeZoom():
|
||||
var newZoom = Vector2(1.0,1.0) / camera.zoom
|
||||
@@ -233,11 +347,45 @@ func changeZoom():
|
||||
pushUpdates.scale = newZoom
|
||||
Global.mouse.scale = newZoom
|
||||
|
||||
# Update UI positions to account for new zoom level
|
||||
# Force update even during panning since zoom affects UI positioning calculations
|
||||
var s = get_viewport().get_visible_rect().size
|
||||
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
||||
tutorial.position = controlPanel.position
|
||||
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
||||
viewerArrows.position = editControls.position
|
||||
pushUpdates.position.y = controlPanel.position.y
|
||||
pushUpdates.position.x = editControls.position.x
|
||||
|
||||
$ControlPanel/ZoomLabel.modulate.a = 6.0
|
||||
$ControlPanel/ZoomLabel.text = "Zoom : " + str(scaleOverall) + "%"
|
||||
|
||||
Global.pushUpdate("Set zoom to " + str(scaleOverall) + "%")
|
||||
onWindowSizeChange()
|
||||
|
||||
## Update camera position with current offset
|
||||
func updateCameraPosition():
|
||||
var s = get_viewport().get_visible_rect().size
|
||||
|
||||
# Only update if not currently panning to avoid conflicts
|
||||
if !isPanning:
|
||||
camera.position = origin.position + cameraOffset
|
||||
|
||||
# Update UI element positions relative to new camera position
|
||||
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
||||
tutorial.position = controlPanel.position
|
||||
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
||||
viewerArrows.position = editControls.position
|
||||
pushUpdates.position.y = controlPanel.position.y
|
||||
pushUpdates.position.x = editControls.position.x
|
||||
|
||||
## Reset camera position to center
|
||||
func resetCameraPosition():
|
||||
cameraOffset = Vector2.ZERO
|
||||
isPanning = false
|
||||
viewPanOffset = Vector2.ZERO
|
||||
viewModePanning = false
|
||||
updateCameraPosition()
|
||||
Global.pushUpdate("Camera reset to center.")
|
||||
|
||||
#When the user speaks!
|
||||
func onSpeak():
|
||||
@@ -252,6 +400,14 @@ func swapMode():
|
||||
editMode = !editMode
|
||||
Global.pushUpdate("Toggled editing mode.")
|
||||
|
||||
# Reset camera when entering view mode
|
||||
if !editMode:
|
||||
resetCameraPosition()
|
||||
else:
|
||||
# Reset view mode panning when entering edit mode
|
||||
viewPanOffset = Vector2.ZERO
|
||||
viewModePanning = false
|
||||
|
||||
get_viewport().transparent_bg = !editMode
|
||||
if Global.backgroundColor.a != 0.0:
|
||||
get_viewport().transparent_bg = false
|
||||
@@ -280,8 +436,17 @@ func add_image(path):
|
||||
|
||||
Global.spriteList.updateData()
|
||||
|
||||
# Sync avatar to NDI viewport
|
||||
if ndi_avatar_viewport:
|
||||
call_deferred("sync_avatar_viewport")
|
||||
|
||||
Global.pushUpdate("Added new sprite.")
|
||||
|
||||
## Sync avatar viewport with current sprite state
|
||||
func sync_avatar_viewport():
|
||||
if ndi_avatar_viewport:
|
||||
ndi_avatar_viewport.sync_avatar()
|
||||
|
||||
#Opens File Dialog
|
||||
func _on_add_button_pressed():
|
||||
fileDialog.visible = true
|
||||
@@ -299,6 +464,11 @@ func _on_load_button_pressed():
|
||||
|
||||
#LOAD AVATAR
|
||||
func _on_load_dialog_file_selected(path):
|
||||
# Handle case where path might be a Dictionary instead of string
|
||||
if typeof(path) == TYPE_DICTIONARY:
|
||||
push_error("Expected string path but got Dictionary: " + str(path))
|
||||
return
|
||||
|
||||
var data = Saving.read_save(path)
|
||||
|
||||
if data == null:
|
||||
@@ -310,8 +480,19 @@ func _on_load_dialog_file_selected(path):
|
||||
origin = new
|
||||
|
||||
for item in data:
|
||||
if typeof(data[item]) != TYPE_DICTIONARY:
|
||||
continue
|
||||
|
||||
if not data[item].has("path"):
|
||||
continue
|
||||
|
||||
var sprite = spriteObject.instantiate()
|
||||
sprite.path = data[item]["path"]
|
||||
|
||||
# Load display name (with backward compatibility)
|
||||
if data[item].has("displayName"):
|
||||
sprite.displayName = data[item]["displayName"]
|
||||
|
||||
sprite.id = data[item]["identification"]
|
||||
sprite.parentId = data[item]["parentId"]
|
||||
|
||||
@@ -342,9 +523,9 @@ func _on_load_dialog_file_selected(path):
|
||||
|
||||
if data[item].has("costumeLayers"):
|
||||
sprite.costumeLayers = str_to_var(data[item]["costumeLayers"]).duplicate()
|
||||
if sprite.costumeLayers.size() < 8:
|
||||
for i in range(5):
|
||||
sprite.costumeLayers.append(1)
|
||||
# Ensure we have exactly 10 costume layers
|
||||
while sprite.costumeLayers.size() < 10:
|
||||
sprite.costumeLayers.append(1)
|
||||
|
||||
if data[item].has("stretchAmount"):
|
||||
sprite.stretchAmount = data[item]["stretchAmount"]
|
||||
@@ -362,6 +543,24 @@ func _on_load_dialog_file_selected(path):
|
||||
sprite.clipped = data[item]["clipped"]
|
||||
if data[item].has("toggle"):
|
||||
sprite.toggle = data[item]["toggle"]
|
||||
if data[item].has("spriteOpacity"):
|
||||
sprite.spriteOpacity = data[item]["spriteOpacity"]
|
||||
if data[item].has("affectChildrenOpacity"):
|
||||
sprite.affectChildrenOpacity = data[item]["affectChildrenOpacity"]
|
||||
if data[item].has("blendMode"):
|
||||
sprite.blendMode = data[item]["blendMode"]
|
||||
|
||||
# Load wobble sync group (with backward compatibility)
|
||||
if data[item].has("wobbleSyncGroup"):
|
||||
sprite.wobbleSyncGroup = data[item]["wobbleSyncGroup"]
|
||||
|
||||
# Load transform properties (with backward compatibility)
|
||||
if data[item].has("staticRotation"):
|
||||
sprite.staticRotation = data[item]["staticRotation"]
|
||||
if data[item].has("mirrorHorizontal"):
|
||||
sprite.mirrorHorizontal = data[item]["mirrorHorizontal"]
|
||||
if data[item].has("mirrorVertical"):
|
||||
sprite.mirrorVertical = data[item]["mirrorVertical"]
|
||||
|
||||
origin.add_child(sprite)
|
||||
sprite.position = str_to_var(data[item]["pos"])
|
||||
@@ -370,10 +569,42 @@ func _on_load_dialog_file_selected(path):
|
||||
Saving.settings["lastAvatar"] = path
|
||||
Global.spriteList.updateData()
|
||||
|
||||
# Load wobble sync groups
|
||||
if data.has("wobbleSyncGroups") and WobbleSyncManager:
|
||||
await WobbleSyncManager.loadSaveData(data["wobbleSyncGroups"])
|
||||
# Refresh wobble sync UI after groups are loaded
|
||||
if Global.spriteEdit and Global.spriteEdit.wobbleSyncControl:
|
||||
Global.spriteEdit.wobbleSyncControl.updateUI()
|
||||
# Force refresh in case timing issues
|
||||
await get_tree().process_frame
|
||||
Global.spriteEdit.wobbleSyncControl.forceRefresh()
|
||||
# Force refresh in case timing issues
|
||||
await get_tree().process_frame
|
||||
Global.spriteEdit.wobbleSyncControl.forceRefresh()
|
||||
|
||||
# Update opacity shaders for all loaded sprites after wobble sync data is applied
|
||||
await get_tree().process_frame # Wait for all sprites to be fully initialized
|
||||
var allSprites = get_tree().get_nodes_in_group("saved")
|
||||
for sprite in allSprites:
|
||||
if sprite.has_method("updateOpacity"):
|
||||
sprite.updateOpacity()
|
||||
|
||||
# Sync avatar to NDI viewport
|
||||
if ndi_avatar_viewport:
|
||||
ndi_avatar_viewport.sync_avatar()
|
||||
|
||||
Global.pushUpdate("Loaded avatar at: " + path)
|
||||
|
||||
onWindowSizeChange()
|
||||
|
||||
|
||||
## Load the default avatar data
|
||||
func loadDefaultAvatar():
|
||||
_on_load_dialog_file_selected("default")
|
||||
|
||||
## Load avatar from a specific file path
|
||||
func loadAvatar(path: String):
|
||||
_on_load_dialog_file_selected(path)
|
||||
|
||||
#SAVE AVATAR
|
||||
func _on_save_dialog_file_selected(path):
|
||||
var data = {}
|
||||
@@ -385,6 +616,7 @@ func _on_save_dialog_file_selected(path):
|
||||
data[id] = {}
|
||||
data[id]["type"] = "sprite"
|
||||
data[id]["path"] = child.path
|
||||
data[id]["displayName"] = child.displayName
|
||||
data[id]["imageData"] = Marshalls.raw_to_base64(child.imageData.save_png_to_buffer())
|
||||
data[id]["identification"] = child.id
|
||||
data[id]["parentId"] = child.parentId
|
||||
@@ -424,8 +656,24 @@ func _on_save_dialog_file_selected(path):
|
||||
|
||||
data[id]["toggle"] = child.toggle
|
||||
|
||||
data[id]["spriteOpacity"] = child.spriteOpacity
|
||||
data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity
|
||||
data[id]["blendMode"] = child.blendMode
|
||||
|
||||
# Save wobble sync group
|
||||
data[id]["wobbleSyncGroup"] = child.wobbleSyncGroup
|
||||
|
||||
# Save transform properties
|
||||
data[id]["staticRotation"] = child.staticRotation
|
||||
data[id]["mirrorHorizontal"] = child.mirrorHorizontal
|
||||
data[id]["mirrorVertical"] = child.mirrorVertical
|
||||
|
||||
id += 1
|
||||
|
||||
# Save wobble sync groups
|
||||
if WobbleSyncManager:
|
||||
data["wobbleSyncGroups"] = WobbleSyncManager.getSaveData()
|
||||
|
||||
Saving.settings["lastAvatar"] = path
|
||||
|
||||
Saving.data = data.duplicate()
|
||||
@@ -445,9 +693,9 @@ func _on_kofi_pressed():
|
||||
Global.pushUpdate("Support me on ko-fi!")
|
||||
|
||||
|
||||
func _on_twitter_pressed():
|
||||
OS.shell_open("https://twitter.com/kaiakairos")
|
||||
Global.pushUpdate("Follow me on twitter!")
|
||||
func _on_bluesky_pressed():
|
||||
OS.shell_open("https://bsky.app/profile/kaiakairos.net")
|
||||
Global.pushUpdate("Follow me on bluesky!")
|
||||
|
||||
|
||||
func _on_replace_button_pressed():
|
||||
@@ -500,9 +748,25 @@ func _on_duplicate_button_pressed():
|
||||
|
||||
sprite.costumeLayers = Global.heldSprite.costumeLayers
|
||||
|
||||
sprite.spriteOpacity = Global.heldSprite.spriteOpacity
|
||||
sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity
|
||||
sprite.blendMode = Global.heldSprite.blendMode
|
||||
|
||||
# Copy wobble sync group
|
||||
sprite.wobbleSyncGroup = Global.heldSprite.wobbleSyncGroup
|
||||
|
||||
# Copy transform properties
|
||||
sprite.staticRotation = Global.heldSprite.staticRotation
|
||||
sprite.mirrorHorizontal = Global.heldSprite.mirrorHorizontal
|
||||
sprite.mirrorVertical = Global.heldSprite.mirrorVertical
|
||||
|
||||
origin.add_child(sprite)
|
||||
sprite.position = Global.heldSprite.position + Vector2(16,16)
|
||||
|
||||
# Update opacity shader for the duplicated sprite
|
||||
await get_tree().process_frame # Wait for sprite to be fully initialized
|
||||
sprite.updateOpacity()
|
||||
|
||||
Global.heldSprite = sprite
|
||||
|
||||
Global.spriteList.updateData()
|
||||
@@ -522,9 +786,11 @@ func changeCostumeStreamDeck(id: String):
|
||||
"9":changeCostume(9)
|
||||
"10":changeCostume(10)
|
||||
|
||||
func changeCostume(newCostume):
|
||||
func changeCostume(newCostume, preserve_selection: bool = false):
|
||||
costume = newCostume
|
||||
Global.heldSprite = null
|
||||
# Only clear heldSprite for actual costume changes, not layer updates
|
||||
if not preserve_selection:
|
||||
Global.heldSprite = null
|
||||
var nodes = get_tree().get_nodes_in_group("saved")
|
||||
for sprite in nodes:
|
||||
if sprite.costumeLayers[newCostume-1] == 1:
|
||||
@@ -547,7 +813,7 @@ func moveSpriteMenu(delta):
|
||||
|
||||
var size = get_viewport().get_visible_rect().size
|
||||
|
||||
var windowLength = 1400
|
||||
var windowLength = 2000
|
||||
|
||||
$ViewerArrows/Arrows.position.y = size.y - 25
|
||||
|
||||
@@ -575,9 +841,9 @@ func moveSpriteMenu(delta):
|
||||
|
||||
|
||||
if $EditControls/MoveMenuUp.overlaps_area(Global.mouse.area):
|
||||
Global.spriteEdit.position.y += (delta*432.0)
|
||||
Global.spriteEdit.position.y += (delta * 400.0)
|
||||
elif $EditControls/MoveMenuDown.overlaps_area(Global.mouse.area):
|
||||
Global.spriteEdit.position.y -= (delta*432.0)
|
||||
Global.spriteEdit.position.y -= (delta * 400.0)
|
||||
|
||||
if Global.spriteEdit.position.y > 66:
|
||||
Global.spriteEdit.position.y = 66
|
||||
@@ -627,47 +893,63 @@ func _on_background_input_capture_bg_key_pressed(node, keys_pressed):
|
||||
if i == KEY_CTRL or i == KEY_SHIFT or i == KEY_ALT or i == KEY_META:
|
||||
continue
|
||||
|
||||
var key_name = OS.get_keycode_string(i) if !OS.get_keycode_string(i).strip_edges().is_empty() else "Keycode" + str(i)
|
||||
keyStrings.append(modifier_prefix + key_name)
|
||||
# Skip mouse button keycodes (common source of phantom inputs)
|
||||
if i == MOUSE_BUTTON_LEFT or i == MOUSE_BUTTON_RIGHT or i == MOUSE_BUTTON_MIDDLE:
|
||||
continue
|
||||
|
||||
# Skip phantom keypad inputs if no actual keypad connected
|
||||
var key_name = OS.get_keycode_string(i)
|
||||
if key_name.begins_with("kp ") or key_name.begins_with("Kp "):
|
||||
# Skip keypad inputs unless they're from actual key presses
|
||||
continue
|
||||
|
||||
if !key_name.strip_edges().is_empty():
|
||||
keyStrings.append(modifier_prefix + key_name)
|
||||
else:
|
||||
keyStrings.append(modifier_prefix + "Keycode" + str(i))
|
||||
|
||||
if fileSystemOpen:
|
||||
return
|
||||
|
||||
if keyStrings.size() <= 0:
|
||||
emit_signal("emptiedCapture")
|
||||
emit_signal("fatfuckingballs")
|
||||
return
|
||||
|
||||
if settingsMenu.awaitingCostumeInput >= 0:
|
||||
|
||||
if keyStrings[0] == "Keycode1":
|
||||
if !settingsMenu.hasMouse:
|
||||
emit_signal("pressedKey")
|
||||
return
|
||||
# Cancel key assignment if user clicks outside the settings menu
|
||||
if keyStrings[0] == "Keycode1" and !settingsMenu.hasMouse:
|
||||
# Don't assign the key, just complete the flow
|
||||
emit_signal("pressedKey")
|
||||
return
|
||||
|
||||
var currentButton = costumeKeys[settingsMenu.awaitingCostumeInput]
|
||||
costumeKeys[settingsMenu.awaitingCostumeInput] = keyStrings[0]
|
||||
Saving.settings["costumeKeys"] = costumeKeys
|
||||
Global.pushUpdate("Changed costume " + str(settingsMenu.awaitingCostumeInput+1) + " hotkey from \"" + currentButton + "\" to \"" + keyStrings[0] + "\"")
|
||||
emit_signal("pressedKey")
|
||||
return
|
||||
|
||||
# Handle sprite visibility toggles if not waiting for costume input
|
||||
spriteVisToggles.emit(keyStrings)
|
||||
|
||||
for key in keyStrings:
|
||||
var i = costumeKeys.find(key)
|
||||
if i >= 0:
|
||||
changeCostume(i+1)
|
||||
|
||||
|
||||
## Simple NDI control function
|
||||
func toggle_ndi():
|
||||
if ndi_output:
|
||||
print("NDI Output is available - Source: ", ndi_output.name)
|
||||
print("NDI should be streaming the main viewport")
|
||||
else:
|
||||
print("NDI Output not found")
|
||||
|
||||
func bgInputSprite(node, keys_pressed):
|
||||
if fileSystemOpen:
|
||||
return
|
||||
var keyStrings = []
|
||||
## Set NDI source name
|
||||
func set_ndi_source_name(source_name: String):
|
||||
if ndi_output:
|
||||
ndi_output.name = source_name
|
||||
print("NDI source name set to: ", source_name)
|
||||
|
||||
for i in keys_pressed:
|
||||
if keys_pressed[i]:
|
||||
keyStrings.append(OS.get_keycode_string(i) if !OS.get_keycode_string(i).strip_edges().is_empty() else "Keycode" + str(i))
|
||||
|
||||
if keyStrings.size() <= 0:
|
||||
emit_signal("fatfuckingballs")
|
||||
return
|
||||
|
||||
spriteVisToggles.emit(keyStrings)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[gd_scene load_steps=80 format=4 uid="uid://bakgnwotiysu1"]
|
||||
[gd_scene load_steps=81 format=4 uid="uid://bakgnwotiysu1"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c4g7pgn40uv4q" path="res://main_scenes/main.gd" id="1_06alx"]
|
||||
[ext_resource type="Script" uid="uid://dkomsdn2pvjwp" path="res://main_scenes/NDIAvatarViewport.gd" id="1_avatar_viewport"]
|
||||
[ext_resource type="Script" uid="uid://18kfbk72spvo" path="res://main_scenes/ControlPanel.gd" id="4_5ygs2"]
|
||||
[ext_resource type="Texture2D" uid="uid://goff8ioh380e" path="res://ui_scenes/button sprites/buttonBack.png" id="4_xenui"]
|
||||
[ext_resource type="Texture2D" uid="uid://da4jthat2oas4" path="res://ui_scenes/button sprites/fancy.png" id="5_75a7k"]
|
||||
@@ -14341,7 +14342,7 @@ offset_bottom = 27.0
|
||||
theme = SubResource("Theme_rv56a")
|
||||
flat = true
|
||||
|
||||
[node name="twitter" type="Button" parent="ControlPanel/Links"]
|
||||
[node name="bluesky" type="Button" parent="ControlPanel/Links"]
|
||||
offset_left = -5.0
|
||||
offset_top = -29.0
|
||||
offset_right = 61.0
|
||||
@@ -14357,37 +14358,70 @@ offset_bottom = -51.0
|
||||
text = "Zoom : 100%"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="VersionLabels" type="Node2D" parent="ControlPanel"]
|
||||
[node name="VersionLabels" type="Control" parent="ControlPanel"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 2
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -870.0
|
||||
offset_top = -107.0
|
||||
offset_right = -610.0
|
||||
offset_bottom = -7.0
|
||||
grow_vertical = 0
|
||||
|
||||
[node name="Label" type="Label" parent="ControlPanel/VersionLabels"]
|
||||
offset_left = -713.0
|
||||
offset_top = -94.0
|
||||
offset_right = -575.0
|
||||
offset_bottom = -60.0
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="ControlPanel/VersionLabels"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 2
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -100.0
|
||||
offset_right = 138.0
|
||||
offset_bottom = 1.0
|
||||
grow_vertical = 0
|
||||
|
||||
[node name="Label" type="Label" parent="ControlPanel/VersionLabels/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "PNGTuber Plus"
|
||||
label_settings = SubResource("LabelSettings_w0f1f")
|
||||
|
||||
[node name="Label2" type="Label" parent="ControlPanel/VersionLabels"]
|
||||
offset_left = -713.0
|
||||
offset_top = -123.0
|
||||
offset_right = -575.0
|
||||
offset_bottom = -89.0
|
||||
[node name="Label2" type="Label" parent="ControlPanel/VersionLabels/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "kaiakairos'"
|
||||
label_settings = SubResource("LabelSettings_qg0do")
|
||||
|
||||
[node name="versionNo" type="Label" parent="ControlPanel/VersionLabels/Label2"]
|
||||
[node name="versionNo" type="Label" parent="ControlPanel/VersionLabels/VBoxContainer/Label2"]
|
||||
layout_mode = 0
|
||||
offset_left = 144.0
|
||||
offset_top = 40.0
|
||||
offset_right = 282.0
|
||||
offset_bottom = 74.0
|
||||
offset_left = 104.0
|
||||
offset_top = 11.0
|
||||
offset_right = 242.0
|
||||
offset_bottom = 54.0
|
||||
text = "v1.4.5"
|
||||
label_settings = SubResource("LabelSettings_xvf50")
|
||||
|
||||
[node name="Label3" type="Label" parent="ControlPanel/VersionLabels/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Litruv Edition"
|
||||
label_settings = SubResource("LabelSettings_qg0do")
|
||||
|
||||
[node name="EditControls" type="Node2D" parent="."]
|
||||
visible = false
|
||||
z_index = 4090
|
||||
script = ExtResource("17_bf1ic")
|
||||
|
||||
[node name="Container" type="Container" parent="EditControls"]
|
||||
clip_contents = true
|
||||
anchors_preset = 9
|
||||
anchor_bottom = 1.0
|
||||
offset_top = 70.0
|
||||
offset_right = 273.485
|
||||
offset_bottom = 20070.0
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="SpriteViewer" parent="EditControls/Container" instance=ExtResource("9_uqv8p")]
|
||||
z_index = 4094
|
||||
position = Vector2(9, 5)
|
||||
|
||||
[node name="Add" type="Sprite2D" parent="EditControls"]
|
||||
position = Vector2(104, 32)
|
||||
texture = ExtResource("4_xenui")
|
||||
@@ -14549,11 +14583,6 @@ position = Vector2(2132, 928.5)
|
||||
shape = SubResource("RectangleShape2D_ebmai")
|
||||
disabled = true
|
||||
|
||||
[node name="SpriteViewer" parent="EditControls" instance=ExtResource("9_uqv8p")]
|
||||
visible = false
|
||||
z_index = 4094
|
||||
position = Vector2(9, 66)
|
||||
|
||||
[node name="MoveMenuUp" type="Area2D" parent="EditControls"]
|
||||
collision_layer = 0
|
||||
collision_mask = 2048
|
||||
@@ -14610,7 +14639,6 @@ patch_margin_right = 8
|
||||
patch_margin_bottom = 8
|
||||
|
||||
[node name="Label" type="Label" parent="Tutorial/NinePatchRect"]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 15.0
|
||||
offset_top = 14.0
|
||||
@@ -14726,11 +14754,29 @@ position = Vector2(0, 720)
|
||||
modulate = Color(0, 0, 0, 0.478431)
|
||||
z_index = -4096
|
||||
|
||||
[node name="NDIOutput" type="NDIOutput" parent="."]
|
||||
name = "PNGTuber-Plus"
|
||||
|
||||
[node name="NDIAvatarViewport" type="SubViewport" parent="."]
|
||||
handle_input_locally = false
|
||||
size = Vector2i(1280, 720)
|
||||
render_target_update_mode = 4
|
||||
script = ExtResource("1_avatar_viewport")
|
||||
|
||||
[node name="NDIOutput" type="NDIOutput" parent="NDIAvatarViewport"]
|
||||
name = "PNGTuber Avatar"
|
||||
enable_editor_output = true
|
||||
|
||||
[node name="OriginMotion" type="Node2D" parent="NDIAvatarViewport"]
|
||||
position = Vector2(640, 360)
|
||||
|
||||
[node name="Origin" type="Node2D" parent="NDIAvatarViewport/OriginMotion"]
|
||||
|
||||
[connection signal="pressed" from="ControlPanel/MicButtong/Button" to="." method="_on_button_pressed"]
|
||||
[connection signal="pressed" from="ControlPanel/SettingsButton/settingsButtons" to="." method="_on_settings_buttons_pressed"]
|
||||
[connection signal="pressed" from="ControlPanel/Edit/Button" to="." method="swapMode"]
|
||||
[connection signal="pressed" from="ControlPanel/Links/kofi" to="." method="_on_kofi_pressed"]
|
||||
[connection signal="pressed" from="ControlPanel/Links/twitter" to="." method="_on_twitter_pressed"]
|
||||
[connection signal="pressed" from="ControlPanel/Links/bluesky" to="." method="_on_bluesky_pressed"]
|
||||
[connection signal="pressed" from="EditControls/Add/addButton" to="." method="_on_add_button_pressed"]
|
||||
[connection signal="pressed" from="EditControls/Link/linkButton" to="." method="_on_link_button_pressed"]
|
||||
[connection signal="pressed" from="EditControls/Exit/Button2" to="." method="swapMode"]
|
||||
@@ -14744,5 +14790,4 @@ z_index = -4096
|
||||
[connection signal="visibility_changed" from="ReplaceDialog" to="." method="_on_replace_dialog_visibility_changed"]
|
||||
[connection signal="file_selected" from="SaveDialog" to="." method="_on_save_dialog_file_selected"]
|
||||
[connection signal="file_selected" from="LoadDialog" to="." method="_on_load_dialog_file_selected"]
|
||||
[connection signal="bg_key_pressed" from="BackgroundInputCapture" to="." method="bgInputSprite"]
|
||||
[connection signal="bg_key_pressed" from="BackgroundInputCapture" to="." method="_on_background_input_capture_bg_key_pressed"]
|
||||
|
||||
@@ -23,11 +23,14 @@ config/icon="res://icon.png"
|
||||
[audio]
|
||||
|
||||
driver/enable_input=true
|
||||
buses/channel_disable_time=0.0
|
||||
|
||||
[autoload]
|
||||
|
||||
Saving="*res://autoload/saving.gd"
|
||||
Global="*res://autoload/global.gd"
|
||||
WobbleSyncManager="*res://autoload/wobble_sync_manager.gd"
|
||||
BlendModeManager="*res://autoload/blend_mode_manager.gd"
|
||||
ElgatoStreamDeck="*res://addons/godot-streamdeck-addon/singleton.gd"
|
||||
DefaultAvatarData="*res://autoload/defaultAvatarData.gd"
|
||||
|
||||
@@ -47,11 +50,6 @@ window/vsync/vsync_mode=0
|
||||
|
||||
project/assembly_name="PNGTuberPlus"
|
||||
|
||||
[editor]
|
||||
|
||||
version_control/plugin_name="GitPlugin"
|
||||
version_control/autoload_on_startup=true
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/godot-streamdeck-addon/plugin.cfg")
|
||||
@@ -135,6 +133,11 @@ control={
|
||||
"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":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
mouse_middle={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
saveImages={
|
||||
"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,"location":0,"echo":false,"script":null)
|
||||
@@ -145,6 +148,6 @@ saveImages={
|
||||
|
||||
textures/canvas_textures/default_texture_filter=0
|
||||
textures/canvas_textures/default_texture_repeat=1
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
renderer/rendering_method="forward_plus"
|
||||
renderer/rendering_method.mobile="forward_plus"
|
||||
environment/defaults/default_clear_color=Color(0.376471, 0.376471, 0.376471, 1)
|
||||
|
||||
BIN
ui_scenes/button sprites/checkmark.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
34
ui_scenes/button sprites/checkmark.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://41u8egcau00i"
|
||||
path="res://.godot/imported/checkmark.png-7bf604eba3017c173b17bad2b166951a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://ui_scenes/button sprites/checkmark.png"
|
||||
dest_files=["res://.godot/imported/checkmark.png-7bf604eba3017c173b17bad2b166951a.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
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
105
ui_scenes/components/IMPLEMENTATION_COMPLETE.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Implementation Complete: Sprite Viewer Slider Components
|
||||
|
||||
## ✅ **Successfully Implemented**
|
||||
|
||||
### **🎯 Components Created**
|
||||
- **LabeledSlider** - Base reusable slider component
|
||||
- **WobbleSlider** - Specialized for wobble parameters with sync support
|
||||
- **ParameterSlider** - Generic parameter binding for sprite properties
|
||||
- **SliderManager** - Centralized management system
|
||||
|
||||
### **🔄 Replaced in sprite_viewer.tscn**
|
||||
|
||||
| **Old Slider** | **New Component** | **Benefits** |
|
||||
|----------------|-------------------|--------------|
|
||||
| Drag Slider | ParameterSlider | Auto label updates, consistent formatting |
|
||||
| X Frequency | WobbleSlider | Sync group support, auto parameter binding |
|
||||
| X Amplitude | WobbleSlider | Consistent wobble handling |
|
||||
| Y Frequency | WobbleSlider | Integrated with wobble sync system |
|
||||
| Y Amplitude | WobbleSlider | Visual feedback for sync status |
|
||||
| Rotation Frequency | WobbleSlider | Proper Hz display formatting |
|
||||
| Rotation Amplitude | WobbleSlider | Percentage display with % suffix |
|
||||
| Rotation Drag | ParameterSlider | Simplified parameter binding |
|
||||
| Squash | ParameterSlider | Decimal precision formatting |
|
||||
| Rotation Limit Min | ParameterSlider | Degree (°) suffix display |
|
||||
| Rotation Limit Max | ParameterSlider | Consistent limit handling |
|
||||
| Animation Frames | ParameterSlider | Integer-only values |
|
||||
| Animation Speed | ParameterSlider | Decimal speed values |
|
||||
| Opacity | ParameterSlider | Custom percentage display |
|
||||
|
||||
### **🎨 Key Features Implemented**
|
||||
|
||||
✅ **Automatic Label Updates** - No more manual label text management
|
||||
✅ **Consistent Formatting** - All sliders use the same styling
|
||||
✅ **Parameter Binding** - Direct sprite property connections
|
||||
✅ **Wobble Sync Integration** - Visual feedback and group handling
|
||||
✅ **Type Safety** - Proper class inheritance
|
||||
✅ **Value Formatting** - Decimal places, suffixes (Hz, %, °)
|
||||
✅ **Signal Management** - Components handle their own connections
|
||||
✅ **Backwards Compatibility** - Old functions cleaned up
|
||||
|
||||
### **🔧 Technical Implementation**
|
||||
|
||||
#### **Sprite Viewer Updates:**
|
||||
- Added SliderManager for centralized control
|
||||
- Implemented `_setup_slider_components()`
|
||||
- Updated `setImage()` to use component system
|
||||
- Modified `updateWobbleControlStates()` for visual sync feedback
|
||||
- Removed redundant old signal handlers
|
||||
- Added new component signal connections
|
||||
|
||||
#### **Scene File Changes:**
|
||||
- Added external resource references for new components
|
||||
- Replaced 14 individual slider+label combinations
|
||||
- Configured each component with appropriate parameters
|
||||
- Removed outdated signal connections
|
||||
- Optimized layout spacing
|
||||
|
||||
#### **Component Features:**
|
||||
- **LabeledSlider**: Base functionality with export properties
|
||||
- **WobbleSlider**: Inherits from LabeledSlider, adds wobble-specific logic
|
||||
- **ParameterSlider**: Generic sprite property binding with special cases
|
||||
- **SliderManager**: Bulk operations and consistent styling
|
||||
|
||||
### **🎯 Benefits Achieved**
|
||||
|
||||
1. **Consistency** - All sliders look and behave identically
|
||||
2. **Maintainability** - One place to modify slider behavior
|
||||
3. **Reusability** - Components work across different scenes
|
||||
4. **Reduced Code** - Less repetitive slider setup and management
|
||||
5. **Better UX** - Automatic updates, proper formatting, visual feedback
|
||||
6. **Type Safety** - Class-based components with proper inheritance
|
||||
|
||||
### **📝 Usage Example**
|
||||
|
||||
**Before (Old System):**
|
||||
```gdscript
|
||||
$WobbleControl/xFrqLabel.text = "x frequency: " + str(value)
|
||||
$WobbleControl/xFrq.value = value
|
||||
# Repeat for every slider...
|
||||
```
|
||||
|
||||
**After (New Components):**
|
||||
```gdscript
|
||||
slider_manager.update_all_sliders(Global.heldSprite)
|
||||
# All sliders update automatically with proper formatting
|
||||
```
|
||||
|
||||
### **🔧 Migration Notes**
|
||||
|
||||
- All old individual sliders have been replaced
|
||||
- Legacy signal handlers removed
|
||||
- Component system handles parameter updates automatically
|
||||
- Visual sync feedback integrated into wobble sliders
|
||||
- Opacity slider has custom percentage display logic
|
||||
|
||||
### **🚀 Ready to Use**
|
||||
|
||||
The sprite viewer now uses a modern, component-based slider system that is:
|
||||
- More maintainable
|
||||
- More consistent
|
||||
- Easier to extend
|
||||
- Better organized
|
||||
- Type-safe
|
||||
|
||||
All sliders in the sprite viewer are now implemented as reusable components! 🎉
|
||||
73
ui_scenes/components/LABEL_FIX.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 🔧 Fixed: Slider Labels Now Show Correct Text
|
||||
|
||||
## ✅ **Problem Solved**
|
||||
|
||||
The issue was that slider labels were showing "Value" instead of the actual `label_text` property (like "X Frequency", "Drag", etc.) in the Godot editor.
|
||||
|
||||
## 🎯 **Root Cause**
|
||||
|
||||
1. **Editor Execution**: Components needed `@tool` to work properly in the editor
|
||||
2. **Initialization Timing**: Label updates happened before exported properties were set
|
||||
3. **Property Setter Timing**: Label wasn't updated when properties were set by the editor
|
||||
|
||||
## 🔧 **Solution Applied**
|
||||
|
||||
### **1. Added `@tool` Directive**
|
||||
Added `@tool` to all component scripts to enable editor execution:
|
||||
- `LabeledSlider.gd`
|
||||
- `WobbleSlider.gd`
|
||||
- `ParameterSlider.gd`
|
||||
|
||||
### **2. Improved Property Setters**
|
||||
Updated all export property setters to check if the node is ready:
|
||||
```gdscript
|
||||
func set_label_text(new_text: String):
|
||||
label_text = new_text
|
||||
if is_inside_tree():
|
||||
_update_label()
|
||||
else:
|
||||
call_deferred("_update_label")
|
||||
```
|
||||
|
||||
### **3. Enhanced Initialization**
|
||||
- Added `NOTIFICATION_SCENE_INSTANTIATED` handler
|
||||
- Improved `_ready()` function timing
|
||||
- Added deferred label updates for proper timing
|
||||
|
||||
### **4. Fixed Base Template**
|
||||
Updated `LabeledSlider.tscn` to have cleaner default text:
|
||||
```
|
||||
text = "Value" # Instead of "Value: 0"
|
||||
```
|
||||
|
||||
### **5. Robust Label Updates**
|
||||
Enhanced `_update_label()` to handle missing nodes gracefully:
|
||||
```gdscript
|
||||
func _update_label():
|
||||
if not label:
|
||||
label = get_node_or_null("Label")
|
||||
if not label:
|
||||
call_deferred("_update_label")
|
||||
return
|
||||
# ... update logic
|
||||
```
|
||||
|
||||
## 🎉 **Result**
|
||||
|
||||
Now in the Godot editor, all slider labels should properly display:
|
||||
- ✅ "X Frequency: 0.000 Hz"
|
||||
- ✅ "Y Amplitude: 0"
|
||||
- ✅ "Drag: 0.0"
|
||||
- ✅ "Opacity: 100%"
|
||||
- ✅ "Rotational Limit Min: -180°"
|
||||
|
||||
Instead of just "Value" everywhere!
|
||||
|
||||
## 🔄 **How to Test**
|
||||
|
||||
1. Open `sprite_viewer.tscn` in the Godot editor
|
||||
2. Check the slider components in the scene tree
|
||||
3. Labels should now show the correct text for each parameter
|
||||
4. Properties can be modified in the inspector and labels update immediately
|
||||
|
||||
The components now work properly both in the editor preview and at runtime! 🚀
|
||||
140
ui_scenes/components/LabeledSlider.gd
Normal file
@@ -0,0 +1,140 @@
|
||||
@tool
|
||||
## A reusable component that combines a label and slider
|
||||
## Provides consistent formatting and behavior for all sliders in the UI
|
||||
extends VBoxContainer
|
||||
class_name LabeledSlider
|
||||
|
||||
## Emitted when the slider value changes
|
||||
signal value_changed(value: float)
|
||||
|
||||
## The base text that appears in the label (without the value)
|
||||
@export var label_text: String = "Value" : set = set_label_text
|
||||
## The minimum value of the slider
|
||||
@export var min_value: float = 0.0 : set = set_min_value
|
||||
## The maximum value of the slider
|
||||
@export var max_value: float = 100.0 : set = set_max_value
|
||||
## The step value for the slider
|
||||
@export var step: float = 1.0 : set = set_step
|
||||
## The current value of the slider
|
||||
@export var value: float = 0.0 : set = set_value
|
||||
## Whether to show the value in the label
|
||||
@export var show_value_in_label: bool = true : set = set_show_value_in_label
|
||||
## Format string for the value display (e.g., "%.1f" for one decimal place)
|
||||
@export var value_format: String = "%.0f" : set = set_value_format
|
||||
## Optional suffix to add after the value (e.g., "%", "Hz")
|
||||
@export var value_suffix: String = "" : set = set_value_suffix
|
||||
## Whether the slider is editable
|
||||
@export var editable: bool = true : set = set_editable
|
||||
|
||||
@onready var label: Label = $Label
|
||||
@onready var slider: HSlider = $HSlider
|
||||
|
||||
func _ready():
|
||||
if slider:
|
||||
slider.value_changed.connect(_on_slider_value_changed)
|
||||
_update_slider_properties()
|
||||
|
||||
# Ensure label is updated after all properties are set
|
||||
call_deferred("_update_label")
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_SCENE_INSTANTIATED:
|
||||
# Update label when scene is instantiated (important for editor)
|
||||
call_deferred("_update_label")
|
||||
|
||||
## Set the label text
|
||||
func set_label_text(new_text: String):
|
||||
label_text = new_text
|
||||
if is_inside_tree():
|
||||
_update_label()
|
||||
else:
|
||||
call_deferred("_update_label")
|
||||
|
||||
## Set the minimum value
|
||||
func set_min_value(new_min: float):
|
||||
min_value = new_min
|
||||
if is_inside_tree():
|
||||
_update_slider_properties()
|
||||
|
||||
## Set the maximum value
|
||||
func set_max_value(new_max: float):
|
||||
max_value = new_max
|
||||
if is_inside_tree():
|
||||
_update_slider_properties()
|
||||
|
||||
## Set the step value
|
||||
func set_step(new_step: float):
|
||||
step = new_step
|
||||
if is_inside_tree():
|
||||
_update_slider_properties()
|
||||
|
||||
## Set the current value
|
||||
func set_value(new_value: float):
|
||||
value = new_value
|
||||
if slider:
|
||||
slider.value = value
|
||||
if is_inside_tree():
|
||||
_update_label()
|
||||
|
||||
## Set whether to show value in label
|
||||
func set_show_value_in_label(show: bool):
|
||||
show_value_in_label = show
|
||||
if is_inside_tree():
|
||||
_update_label()
|
||||
|
||||
## Set the value format string
|
||||
func set_value_format(format: String):
|
||||
value_format = format
|
||||
if is_inside_tree():
|
||||
_update_label()
|
||||
|
||||
## Set the value suffix
|
||||
func set_value_suffix(suffix: String):
|
||||
value_suffix = suffix
|
||||
if is_inside_tree():
|
||||
_update_label()
|
||||
|
||||
## Set whether the slider is editable
|
||||
func set_editable(is_editable: bool):
|
||||
editable = is_editable
|
||||
if slider:
|
||||
slider.editable = editable
|
||||
|
||||
## Update the slider properties
|
||||
func _update_slider_properties():
|
||||
if not slider:
|
||||
return
|
||||
|
||||
slider.min_value = min_value
|
||||
slider.max_value = max_value
|
||||
slider.step = step
|
||||
slider.value = value
|
||||
slider.editable = editable
|
||||
|
||||
## Update the label text
|
||||
func _update_label():
|
||||
if not label:
|
||||
# If label isn't ready yet, try to find it
|
||||
label = get_node_or_null("Label")
|
||||
if not label:
|
||||
# Call again after a frame when nodes are ready
|
||||
call_deferred("_update_label")
|
||||
return
|
||||
|
||||
var text = label_text
|
||||
if show_value_in_label:
|
||||
var formatted_value = value_format % value
|
||||
text += ": " + formatted_value + value_suffix
|
||||
|
||||
label.text = text
|
||||
|
||||
## Handle slider value changes
|
||||
func _on_slider_value_changed(new_value: float):
|
||||
value = new_value
|
||||
_update_label()
|
||||
value_changed.emit(value)
|
||||
|
||||
## Set the modulate color for visual feedback
|
||||
func set_modulate_color(color: Color):
|
||||
if slider:
|
||||
slider.modulate = color
|
||||
1
ui_scenes/components/LabeledSlider.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://combq5rrok31i
|
||||
22
ui_scenes/components/LabeledSlider.tscn
Normal file
@@ -0,0 +1,22 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bxm5q6n8x7p2q"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://combq5rrok31i" path="res://ui_scenes/components/LabeledSlider.gd" id="1_labeled_slider"]
|
||||
|
||||
[node name="LabeledSlider" type="VBoxContainer"]
|
||||
offset_right = 200.0
|
||||
offset_bottom = 40.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
theme_override_constants/separation = 2
|
||||
script = ExtResource("1_labeled_slider")
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
text = "Value: 0"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="HSlider" type="HSlider" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
81
ui_scenes/components/ParameterSlider.gd
Normal file
@@ -0,0 +1,81 @@
|
||||
@tool
|
||||
## A parameter slider component for general sprite parameters
|
||||
## Handles updating sprite properties with proper formatting
|
||||
extends LabeledSlider
|
||||
class_name ParameterSlider
|
||||
|
||||
## The sprite parameter name this slider controls
|
||||
@export var parameter_name: String = ""
|
||||
## The property path in the sprite object (e.g., "dragSpeed", "rdragStr")
|
||||
@export var sprite_property: String = ""
|
||||
|
||||
## Reference to the sprite being edited
|
||||
var target_sprite = null
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
# Connect to value changes to update parameters
|
||||
value_changed.connect(_on_parameter_value_changed)
|
||||
|
||||
func _notification(what):
|
||||
super._notification(what)
|
||||
if what == NOTIFICATION_SCENE_INSTANTIATED:
|
||||
# Update label for opacity specially
|
||||
if sprite_property == "spriteOpacity":
|
||||
call_deferred("_update_opacity_label")
|
||||
|
||||
## Set the sprite this slider controls
|
||||
func set_target_sprite(sprite):
|
||||
target_sprite = sprite
|
||||
if sprite:
|
||||
_update_from_sprite()
|
||||
|
||||
## Update the slider from the sprite's current values
|
||||
func _update_from_sprite():
|
||||
if not target_sprite or sprite_property == "":
|
||||
return
|
||||
|
||||
var current_value = target_sprite.get(sprite_property)
|
||||
if current_value != null:
|
||||
set_value(current_value)
|
||||
|
||||
# Special handling for opacity display
|
||||
if sprite_property == "spriteOpacity":
|
||||
_update_opacity_label()
|
||||
|
||||
## Update label text with special opacity handling
|
||||
func _update_opacity_label():
|
||||
if sprite_property == "spriteOpacity" and label:
|
||||
var percentage = int(value * 100)
|
||||
label.text = label_text + ": " + str(percentage) + "%"
|
||||
|
||||
## Override base update_label for special cases
|
||||
func _update_label():
|
||||
if sprite_property == "spriteOpacity":
|
||||
_update_opacity_label()
|
||||
else:
|
||||
super._update_label()
|
||||
|
||||
## Handle parameter changes
|
||||
func _on_parameter_value_changed(new_value: float):
|
||||
if not target_sprite or sprite_property == "":
|
||||
return
|
||||
|
||||
|
||||
target_sprite.set(sprite_property, new_value)
|
||||
|
||||
# Call specific update methods if they exist
|
||||
_call_update_method()
|
||||
|
||||
## Call specific update methods for certain parameters
|
||||
func _call_update_method():
|
||||
if not target_sprite:
|
||||
return
|
||||
|
||||
match sprite_property:
|
||||
"spriteOpacity":
|
||||
target_sprite.updateOpacity()
|
||||
"frames":
|
||||
target_sprite.changeFrames()
|
||||
"blendMode":
|
||||
target_sprite.updateBlendMode(value)
|
||||
1
ui_scenes/components/ParameterSlider.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://6maee7dhl7vl
|
||||
9
ui_scenes/components/ParameterSlider.tscn
Normal file
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://fgee1pq0yqav"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bxm5q6n8x7p2q" path="res://ui_scenes/components/LabeledSlider.tscn" id="1_base_slider"]
|
||||
[ext_resource type="Script" uid="uid://6maee7dhl7vl" path="res://ui_scenes/components/ParameterSlider.gd" id="2_parameter_script"]
|
||||
|
||||
[node name="ParameterSlider" instance=ExtResource("1_base_slider")]
|
||||
script = ExtResource("2_parameter_script")
|
||||
parameter_name = ""
|
||||
sprite_property = ""
|
||||
220
ui_scenes/components/README.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Slider Components Documentation
|
||||
|
||||
This document explains the new slider component system that replaces the individual sliders in the sprite viewer with reusable, consistent components.
|
||||
|
||||
## Components Overview
|
||||
|
||||
### 1. LabeledSlider (Base Component)
|
||||
**File:** `ui_scenes/components/LabeledSlider.tscn`
|
||||
**Script:** `ui_scenes/components/LabeledSlider.gd`
|
||||
|
||||
The base slider component that combines a label and slider into one reusable unit.
|
||||
|
||||
**Key Features:**
|
||||
- Automatic label updates with current value
|
||||
- Configurable value formatting
|
||||
- Optional value suffix (%, Hz, etc.)
|
||||
- Consistent styling
|
||||
- Signal emission on value changes
|
||||
|
||||
**Properties:**
|
||||
- `label_text`: Base text for the label
|
||||
- `min_value`, `max_value`, `step`: Slider configuration
|
||||
- `value`: Current value
|
||||
- `show_value_in_label`: Whether to display value in label
|
||||
- `value_format`: Format string for value display (e.g., "%.1f")
|
||||
- `value_suffix`: Suffix to add after value (e.g., "%", "Hz")
|
||||
- `editable`: Whether the slider can be modified
|
||||
|
||||
### 2. WobbleSlider (Specialized Component)
|
||||
**File:** `ui_scenes/components/WobbleSlider.tscn`
|
||||
**Script:** `ui_scenes/components/WobbleSlider.gd`
|
||||
|
||||
Extends LabeledSlider specifically for wobble parameters.
|
||||
|
||||
**Key Features:**
|
||||
- Automatic synchronization with sprite wobble parameters
|
||||
- Visual feedback for sync group status
|
||||
- Integrated with wobble sync system
|
||||
- Parameter-specific configuration
|
||||
|
||||
**Properties:**
|
||||
- `parameter_name`: The wobble parameter this controls ("xFrq", "xAmp", etc.)
|
||||
- All LabeledSlider properties
|
||||
|
||||
### 3. ParameterSlider (Generic Component)
|
||||
**File:** `ui_scenes/components/ParameterSlider.tscn`
|
||||
**Script:** `ui_scenes/components/ParameterSlider.gd`
|
||||
|
||||
Extends LabeledSlider for general sprite parameters.
|
||||
|
||||
**Key Features:**
|
||||
- Generic parameter binding to sprite properties
|
||||
- Automatic update method calling for specific parameters
|
||||
- Property path configuration
|
||||
|
||||
**Properties:**
|
||||
- `sprite_property`: The sprite property to bind to
|
||||
- `parameter_name`: Display name for the parameter
|
||||
- All LabeledSlider properties
|
||||
|
||||
### 4. WobbleControlPanel (Composite Component)
|
||||
**File:** `ui_scenes/components/WobbleControlPanel.tscn`
|
||||
**Script:** `ui_scenes/components/WobbleControlPanel.gd`
|
||||
|
||||
A complete panel containing all wobble sliders.
|
||||
|
||||
**Key Features:**
|
||||
- Pre-configured wobble sliders
|
||||
- Centralized sprite management
|
||||
- Integrated sync feedback
|
||||
- Easy integration with existing wobble sync controls
|
||||
|
||||
### 5. SliderManager (Management System)
|
||||
**Script:** `ui_scenes/components/SliderManager.gd`
|
||||
|
||||
Manages collections of slider components.
|
||||
|
||||
**Key Features:**
|
||||
- Centralized slider updates
|
||||
- Consistent styling application
|
||||
- Bulk operations on sliders
|
||||
- Integration with sprite viewer
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic LabeledSlider
|
||||
```gdscript
|
||||
# In your scene
|
||||
@onready var my_slider: LabeledSlider = $LabeledSlider
|
||||
|
||||
func _ready():
|
||||
my_slider.label_text = "Volume"
|
||||
my_slider.min_value = 0.0
|
||||
my_slider.max_value = 100.0
|
||||
my_slider.value_suffix = "%"
|
||||
my_slider.value_changed.connect(_on_volume_changed)
|
||||
|
||||
func _on_volume_changed(value: float):
|
||||
print("Volume changed to: ", value)
|
||||
```
|
||||
|
||||
### WobbleSlider Configuration
|
||||
```gdscript
|
||||
# In scene setup
|
||||
@onready var x_freq_slider: WobbleSlider = $XFrequencySlider
|
||||
|
||||
func _ready():
|
||||
x_freq_slider.label_text = "X Frequency"
|
||||
x_freq_slider.parameter_name = "xFrq"
|
||||
x_freq_slider.min_value = 0.0
|
||||
x_freq_slider.max_value = 10.0
|
||||
x_freq_slider.step = 0.1
|
||||
x_freq_slider.value_format = "%.1f"
|
||||
x_freq_slider.value_suffix = " Hz"
|
||||
|
||||
func set_sprite(sprite):
|
||||
x_freq_slider.set_target_sprite(sprite)
|
||||
```
|
||||
|
||||
### Using WobbleControlPanel
|
||||
```gdscript
|
||||
# Replace individual wobble sliders with the panel
|
||||
@onready var wobble_panel: WobbleControlPanel = $WobbleControlPanel
|
||||
|
||||
func setImage():
|
||||
if Global.heldSprite:
|
||||
wobble_panel.set_target_sprite(Global.heldSprite)
|
||||
wobble_panel.update_from_sprite()
|
||||
```
|
||||
|
||||
### SliderManager Integration
|
||||
```gdscript
|
||||
# In sprite viewer
|
||||
var slider_manager: SliderManager
|
||||
|
||||
func _ready():
|
||||
slider_manager = SliderManager.new(self)
|
||||
_setup_sliders()
|
||||
|
||||
func _setup_sliders():
|
||||
# Add sliders to manager
|
||||
slider_manager.add_slider($DragSlider)
|
||||
slider_manager.add_slider($OpacitySlider)
|
||||
# etc.
|
||||
|
||||
func setImage():
|
||||
if slider_manager:
|
||||
slider_manager.update_all_sliders(Global.heldSprite)
|
||||
```
|
||||
|
||||
## Migration from Legacy Sliders
|
||||
|
||||
### Step 1: Replace Scene Nodes
|
||||
Replace individual HSlider + Label combinations with LabeledSlider instances:
|
||||
|
||||
**Before:**
|
||||
```
|
||||
- Node2D
|
||||
- Label (for "x frequency: 5.0")
|
||||
- HSlider (for value input)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```
|
||||
- Node2D
|
||||
- LabeledSlider (handles both label and slider)
|
||||
```
|
||||
|
||||
### Step 2: Update Script References
|
||||
**Before:**
|
||||
```gdscript
|
||||
$WobbleControl/xFrqLabel.text = "x frequency: " + str(value)
|
||||
$WobbleControl/xFrq.value = value
|
||||
```
|
||||
|
||||
**After:**
|
||||
```gdscript
|
||||
$WobbleControl/XFrequencySlider.set_value(value)
|
||||
# Label updates automatically
|
||||
```
|
||||
|
||||
### Step 3: Connect Signals
|
||||
**Before:**
|
||||
```gdscript
|
||||
func _on_x_frq_value_changed(value):
|
||||
Global.heldSprite.xFrq = value
|
||||
```
|
||||
|
||||
**After:**
|
||||
```gdscript
|
||||
# Handled automatically by WobbleSlider component
|
||||
# Or connect to the component's value_changed signal
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Consistency**: All sliders look and behave the same way
|
||||
2. **Maintainability**: Changes to slider behavior only need to be made in one place
|
||||
3. **Reusability**: Components can be used across different parts of the application
|
||||
4. **Type Safety**: Proper class inheritance with specific functionality
|
||||
5. **Reduced Code**: Less repetitive slider setup code
|
||||
6. **Better Organization**: Clear separation of concerns
|
||||
|
||||
## Integration Notes
|
||||
|
||||
- The new system is designed to be backwards compatible
|
||||
- Legacy slider code is maintained as fallback
|
||||
- Components can be gradually adopted
|
||||
- SliderManager provides centralized control
|
||||
- Easy to extend for new parameter types
|
||||
|
||||
## Custom Styling
|
||||
|
||||
To apply consistent styling across all sliders, modify the base LabeledSlider component or use the SliderManager's styling methods:
|
||||
|
||||
```gdscript
|
||||
func apply_custom_theme():
|
||||
for slider in slider_manager.sliders:
|
||||
slider.slider.add_theme_stylebox_override("slider", custom_style)
|
||||
```
|
||||
47
ui_scenes/components/SampleWobbleControl.tscn
Normal file
@@ -0,0 +1,47 @@
|
||||
[gd_scene load_steps=4 format=4 uid="uid://bh7n2k1q5m8ya"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://c8q7x2n9p4r5s" path="res://ui_scenes/components/WobbleSlider.tscn" id="1_wobble_x_frq"]
|
||||
[ext_resource type="PackedScene" uid="uid://c8q7x2n9p4r5s" path="res://ui_scenes/components/WobbleSlider.tscn" id="2_wobble_x_amp"]
|
||||
[ext_resource type="PackedScene" uid="uid://d9t8y3m0k6l1n" path="res://ui_scenes/components/ParameterSlider.tscn" id="3_drag_slider"]
|
||||
|
||||
[node name="SampleWobbleControl" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_right = -1680.0
|
||||
offset_bottom = -1080.0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
|
||||
[node name="XFrequencySlider" parent="VBoxContainer" instance=ExtResource("1_wobble_x_frq")]
|
||||
layout_mode = 2
|
||||
label_text = "X Frequency"
|
||||
min_value = 0.0
|
||||
max_value = 10.0
|
||||
step = 0.1
|
||||
value_format = "%.1f"
|
||||
value_suffix = " Hz"
|
||||
parameter_name = "xFrq"
|
||||
|
||||
[node name="XAmplitudeSlider" parent="VBoxContainer" instance=ExtResource("2_wobble_x_amp")]
|
||||
layout_mode = 2
|
||||
label_text = "X Amplitude"
|
||||
min_value = 0.0
|
||||
max_value = 100.0
|
||||
step = 1.0
|
||||
value_format = "%.0f"
|
||||
parameter_name = "xAmp"
|
||||
|
||||
[node name="DragSlider" parent="VBoxContainer" instance=ExtResource("3_drag_slider")]
|
||||
layout_mode = 2
|
||||
label_text = "Drag"
|
||||
min_value = 0.0
|
||||
max_value = 10.0
|
||||
step = 0.1
|
||||
value_format = "%.1f"
|
||||
sprite_property = "dragSpeed"
|
||||
0
ui_scenes/components/SliderConfig.gd
Normal file
1
ui_scenes/components/SliderConfig.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://db4gh5lnm7j5k
|
||||
60
ui_scenes/components/SliderManager.gd
Normal file
@@ -0,0 +1,60 @@
|
||||
## Manages all slider components in the sprite viewer
|
||||
## Provides a centralized way to update and configure sliders
|
||||
extends RefCounted
|
||||
class_name SliderManager
|
||||
|
||||
## Array of all slider components
|
||||
var sliders: Array[LabeledSlider] = []
|
||||
## Reference to the sprite viewer
|
||||
var sprite_viewer = null
|
||||
|
||||
func _init(viewer):
|
||||
sprite_viewer = viewer
|
||||
|
||||
## Add a slider to be managed
|
||||
func add_slider(slider: LabeledSlider):
|
||||
if slider not in sliders:
|
||||
sliders.append(slider)
|
||||
|
||||
## Remove a slider from management
|
||||
func remove_slider(slider: LabeledSlider):
|
||||
sliders.erase(slider)
|
||||
|
||||
## Update all sliders with the current sprite data
|
||||
func update_all_sliders(sprite):
|
||||
for slider in sliders:
|
||||
if slider is WobbleSlider:
|
||||
slider.set_target_sprite(sprite)
|
||||
elif slider is ParameterSlider:
|
||||
slider.set_target_sprite(sprite)
|
||||
|
||||
## Update wobble control states based on sync status
|
||||
func update_wobble_control_states(sprite):
|
||||
if not sprite:
|
||||
return
|
||||
|
||||
var is_synced = sprite.isSynced()
|
||||
|
||||
for slider in sliders:
|
||||
if slider is WobbleSlider:
|
||||
# Keep wobble controls enabled even when synced (they edit group settings)
|
||||
slider.set_editable(true)
|
||||
|
||||
# Visual feedback for sync state - slight tint to indicate group editing
|
||||
var tint = Color(1.0, 1.0, 0.9) if is_synced else Color.WHITE
|
||||
slider.set_modulate_color(tint)
|
||||
|
||||
## Get a slider by its parameter name
|
||||
func get_slider_by_parameter(parameter_name: String) -> LabeledSlider:
|
||||
for slider in sliders:
|
||||
if slider is WobbleSlider and slider.parameter_name == parameter_name:
|
||||
return slider
|
||||
elif slider is ParameterSlider and slider.sprite_property == parameter_name:
|
||||
return slider
|
||||
return null
|
||||
|
||||
## Apply consistent styling to all sliders
|
||||
func apply_consistent_styling():
|
||||
for slider in sliders:
|
||||
# Apply any consistent styling here
|
||||
pass
|
||||
1
ui_scenes/components/SliderManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c0rejv3uadhfh
|
||||
0
ui_scenes/components/SpriteSliderFactory.gd
Normal file
1
ui_scenes/components/SpriteSliderFactory.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c8ssre2afcija
|
||||
63
ui_scenes/components/WobbleControlPanel.gd
Normal file
@@ -0,0 +1,63 @@
|
||||
## A complete wobble control panel using the new slider components
|
||||
## Provides a consistent interface for all wobble parameters
|
||||
extends Control
|
||||
class_name WobbleControlPanel
|
||||
|
||||
## Reference to all wobble sliders
|
||||
@onready var x_frequency_slider: WobbleSlider = $VBoxContainer/XFrequencySlider
|
||||
@onready var x_amplitude_slider: WobbleSlider = $VBoxContainer/XAmplitudeSlider
|
||||
@onready var y_frequency_slider: WobbleSlider = $VBoxContainer/YFrequencySlider
|
||||
@onready var y_amplitude_slider: WobbleSlider = $VBoxContainer/YAmplitudeSlider
|
||||
@onready var r_frequency_slider: WobbleSlider = $VBoxContainer/RFrequencySlider
|
||||
@onready var r_amplitude_slider: WobbleSlider = $VBoxContainer/RAmplitudeSlider
|
||||
|
||||
## Array of all wobble sliders for easy iteration
|
||||
var wobble_sliders: Array[WobbleSlider] = []
|
||||
|
||||
## The sprite currently being edited
|
||||
var target_sprite = null
|
||||
|
||||
func _ready():
|
||||
# Collect all wobble sliders
|
||||
wobble_sliders = [
|
||||
x_frequency_slider,
|
||||
x_amplitude_slider,
|
||||
y_frequency_slider,
|
||||
y_amplitude_slider,
|
||||
r_frequency_slider,
|
||||
r_amplitude_slider
|
||||
]
|
||||
|
||||
# Connect to wobble sync control if it exists
|
||||
var wobble_sync_control = find_child("WobbleSyncControl")
|
||||
if wobble_sync_control:
|
||||
_connect_wobble_sync_signals(wobble_sync_control)
|
||||
|
||||
## Set the target sprite for all sliders
|
||||
func set_target_sprite(sprite):
|
||||
target_sprite = sprite
|
||||
for slider in wobble_sliders:
|
||||
slider.set_target_sprite(sprite)
|
||||
|
||||
## Update all sliders from the current sprite
|
||||
func update_from_sprite():
|
||||
if target_sprite:
|
||||
for slider in wobble_sliders:
|
||||
slider._update_from_sprite()
|
||||
|
||||
## Update visual feedback for sync status
|
||||
func update_sync_feedback():
|
||||
for slider in wobble_sliders:
|
||||
slider._update_sync_feedback()
|
||||
|
||||
## Connect signals from wobble sync control
|
||||
func _connect_wobble_sync_signals(wobble_sync_control):
|
||||
# Connect value change signals to update wobble sync control
|
||||
for slider in wobble_sliders:
|
||||
slider.value_changed.connect(_on_wobble_parameter_changed)
|
||||
|
||||
## Handle wobble parameter changes to update sync control
|
||||
func _on_wobble_parameter_changed(value: float):
|
||||
var wobble_sync_control = find_child("WobbleSyncControl")
|
||||
if wobble_sync_control and wobble_sync_control.has_method("updateUI"):
|
||||
wobble_sync_control.updateUI()
|
||||
1
ui_scenes/components/WobbleControlPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cq2ltb5rxsr08
|
||||
75
ui_scenes/components/WobbleControlPanel.tscn
Normal file
@@ -0,0 +1,75 @@
|
||||
[gd_scene load_steps=8 format=4 uid="uid://bp2q8r4l6lam3"]
|
||||
|
||||
[ext_resource type="Script" path="res://ui_scenes/components/WobbleControlPanel.gd" id="1_wobble_panel_script"]
|
||||
[ext_resource type="PackedScene" uid="uid://c8q7x2n9p4r5s" path="res://ui_scenes/components/WobbleSlider.tscn" id="2_wobble_slider"]
|
||||
|
||||
[node name="WobbleControlPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1_wobble_panel_script")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
|
||||
[node name="XFrequencySlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
|
||||
layout_mode = 2
|
||||
label_text = "X Frequency"
|
||||
min_value = 0.0
|
||||
max_value = 10.0
|
||||
step = 0.1
|
||||
value_format = "%.1f"
|
||||
value_suffix = " Hz"
|
||||
parameter_name = "xFrq"
|
||||
|
||||
[node name="XAmplitudeSlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
|
||||
layout_mode = 2
|
||||
label_text = "X Amplitude"
|
||||
min_value = 0.0
|
||||
max_value = 100.0
|
||||
step = 1.0
|
||||
value_format = "%.0f"
|
||||
parameter_name = "xAmp"
|
||||
|
||||
[node name="YFrequencySlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
|
||||
layout_mode = 2
|
||||
label_text = "Y Frequency"
|
||||
min_value = 0.0
|
||||
max_value = 10.0
|
||||
step = 0.1
|
||||
value_format = "%.1f"
|
||||
value_suffix = " Hz"
|
||||
parameter_name = "yFrq"
|
||||
|
||||
[node name="YAmplitudeSlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
|
||||
layout_mode = 2
|
||||
label_text = "Y Amplitude"
|
||||
min_value = 0.0
|
||||
max_value = 100.0
|
||||
step = 1.0
|
||||
value_format = "%.0f"
|
||||
parameter_name = "yAmp"
|
||||
|
||||
[node name="RFrequencySlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
|
||||
layout_mode = 2
|
||||
label_text = "Rotation Frequency"
|
||||
min_value = 0.0
|
||||
max_value = 10.0
|
||||
step = 0.1
|
||||
value_format = "%.1f"
|
||||
value_suffix = " Hz"
|
||||
parameter_name = "rFrq"
|
||||
|
||||
[node name="RAmplitudeSlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
|
||||
layout_mode = 2
|
||||
label_text = "Rotation Amplitude"
|
||||
min_value = 0.0
|
||||
max_value = 100.0
|
||||
step = 1.0
|
||||
value_format = "%.0f"
|
||||
value_suffix = "%"
|
||||
parameter_name = "rAmp"
|
||||
60
ui_scenes/components/WobbleSlider.gd
Normal file
@@ -0,0 +1,60 @@
|
||||
@tool
|
||||
## A specialized slider component for wobble parameters
|
||||
## Automatically handles syncing with wobble groups and provides appropriate feedback
|
||||
extends LabeledSlider
|
||||
class_name WobbleSlider
|
||||
|
||||
## The wobble parameter name this slider controls
|
||||
@export var parameter_name: String = ""
|
||||
|
||||
## Reference to the sprite being edited
|
||||
var target_sprite = null
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
# Connect to value changes to update wobble parameters
|
||||
value_changed.connect(_on_wobble_value_changed)
|
||||
|
||||
## Set the sprite this slider controls
|
||||
func set_target_sprite(sprite):
|
||||
target_sprite = sprite
|
||||
if sprite:
|
||||
_update_from_sprite()
|
||||
|
||||
## Update the slider from the sprite's current values
|
||||
func _update_from_sprite():
|
||||
if not target_sprite or parameter_name == "":
|
||||
return
|
||||
|
||||
match parameter_name:
|
||||
"xFrq":
|
||||
set_value(target_sprite.xFrq)
|
||||
"xAmp":
|
||||
set_value(target_sprite.xAmp)
|
||||
"yFrq":
|
||||
set_value(target_sprite.yFrq)
|
||||
"yAmp":
|
||||
set_value(target_sprite.yAmp)
|
||||
"rFrq":
|
||||
set_value(target_sprite.rFrq)
|
||||
"rAmp":
|
||||
set_value(target_sprite.rAmp)
|
||||
|
||||
## Handle wobble parameter changes
|
||||
func _on_wobble_value_changed(new_value: float):
|
||||
if not target_sprite or parameter_name == "":
|
||||
return
|
||||
|
||||
target_sprite.updateWobbleParameter(parameter_name, new_value)
|
||||
|
||||
# Update visual feedback for sync status
|
||||
_update_sync_feedback()
|
||||
|
||||
## Update visual feedback based on sync status
|
||||
func _update_sync_feedback():
|
||||
if not target_sprite:
|
||||
return
|
||||
|
||||
var is_synced = target_sprite.isSynced()
|
||||
var tint = Color(1.0, 1.0, 0.9) if is_synced else Color.WHITE
|
||||
set_modulate_color(tint)
|
||||
1
ui_scenes/components/WobbleSlider.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://fsc2h7ajjcbs
|
||||
19
ui_scenes/components/WobbleSlider.tscn
Normal file
@@ -0,0 +1,19 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://c8q7x2oap4r5s"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bxm5q6n8x7p2q" path="res://ui_scenes/components/LabeledSlider.tscn" id="1_base_slider"]
|
||||
[ext_resource type="Script" uid="uid://fsc2h7ajjcbs" path="res://ui_scenes/components/WobbleSlider.gd" id="2_wobble_script"]
|
||||
|
||||
[node name="WobbleSlider" instance=ExtResource("1_base_slider")]
|
||||
script = ExtResource("2_wobble_script")
|
||||
parameter_name = ""
|
||||
|
||||
[node name="Label" parent="." index="0"]
|
||||
anchors_preset = 10
|
||||
grow_horizontal = 2
|
||||
text = "Value: 0"
|
||||
horizontal_alignment = 0
|
||||
|
||||
[node name="HSlider" parent="." index="1"]
|
||||
anchors_preset = 12
|
||||
grow_horizontal = 0
|
||||
grow_vertical = 0
|
||||
@@ -7,17 +7,11 @@ func _ready():
|
||||
|
||||
|
||||
func _on_button_pressed():
|
||||
|
||||
if !get_parent().get_parent().get_parent().visible:
|
||||
return
|
||||
|
||||
AudioServer.input_device = micName
|
||||
Global.deleteAllMics()
|
||||
Global.currentMicrophone = null
|
||||
|
||||
Global.createMicrophone()
|
||||
get_parent().get_parent().get_parent().visible = false
|
||||
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
Global.createMicrophone()
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ func _ready():
|
||||
Global.mouse = self
|
||||
|
||||
func _process(delta):
|
||||
if Global.main.editMode:
|
||||
if Global.main and Global.main.editMode:
|
||||
if text != "":
|
||||
label.text = text
|
||||
visible = true
|
||||
|
||||
8
ui_scenes/selectedSprite/opacity.gdshader
Normal file
@@ -0,0 +1,8 @@
|
||||
shader_type canvas_item;
|
||||
|
||||
uniform float opacity : hint_range(0.0, 1.0) = 1.0;
|
||||
|
||||
void fragment() {
|
||||
COLOR = texture(TEXTURE, UV);
|
||||
COLOR.a *= opacity;
|
||||
}
|
||||
1
ui_scenes/selectedSprite/opacity.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://byxs44vljsg2f
|
||||
122
ui_scenes/selectedSprite/opacity_blend.gdshader
Normal file
@@ -0,0 +1,122 @@
|
||||
shader_type canvas_item;
|
||||
|
||||
uniform float opacity : hint_range(0.0, 1.0) = 1.0;
|
||||
uniform int blend_mode : hint_range(0, 11) = 0;
|
||||
|
||||
// Blend mode constants:
|
||||
// 0: Normal
|
||||
// 1: Multiply
|
||||
// 2: Screen
|
||||
// 3: Overlay
|
||||
// 4: Soft Light
|
||||
// 5: Hard Light
|
||||
// 6: Color Dodge
|
||||
// 7: Color Burn
|
||||
// 8: Darken
|
||||
// 9: Lighten
|
||||
// 10: Add (Linear Dodge)
|
||||
// 11: Subtract
|
||||
|
||||
vec3 multiply_blend(vec3 base, vec3 overlay) {
|
||||
return base * overlay;
|
||||
}
|
||||
|
||||
vec3 screen_blend(vec3 base, vec3 overlay) {
|
||||
return 1.0 - (1.0 - base) * (1.0 - overlay);
|
||||
}
|
||||
|
||||
vec3 overlay_blend(vec3 base, vec3 overlay) {
|
||||
vec3 result;
|
||||
result.r = base.r < 0.5 ? 2.0 * base.r * overlay.r : 1.0 - 2.0 * (1.0 - base.r) * (1.0 - overlay.r);
|
||||
result.g = base.g < 0.5 ? 2.0 * base.g * overlay.g : 1.0 - 2.0 * (1.0 - base.g) * (1.0 - overlay.g);
|
||||
result.b = base.b < 0.5 ? 2.0 * base.b * overlay.b : 1.0 - 2.0 * (1.0 - base.b) * (1.0 - overlay.b);
|
||||
return result;
|
||||
}
|
||||
|
||||
vec3 soft_light_blend(vec3 base, vec3 overlay) {
|
||||
vec3 result;
|
||||
result.r = overlay.r < 0.5 ? 2.0 * base.r * overlay.r + base.r * base.r * (1.0 - 2.0 * overlay.r) :
|
||||
sqrt(base.r) * (2.0 * overlay.r - 1.0) + 2.0 * base.r * (1.0 - overlay.r);
|
||||
result.g = overlay.g < 0.5 ? 2.0 * base.g * overlay.g + base.g * base.g * (1.0 - 2.0 * overlay.g) :
|
||||
sqrt(base.g) * (2.0 * overlay.g - 1.0) + 2.0 * base.g * (1.0 - overlay.g);
|
||||
result.b = overlay.b < 0.5 ? 2.0 * base.b * overlay.b + base.b * base.b * (1.0 - 2.0 * overlay.b) :
|
||||
sqrt(base.b) * (2.0 * overlay.b - 1.0) + 2.0 * base.b * (1.0 - overlay.b);
|
||||
return result;
|
||||
}
|
||||
|
||||
vec3 hard_light_blend(vec3 base, vec3 overlay) {
|
||||
vec3 result;
|
||||
result.r = overlay.r < 0.5 ? 2.0 * base.r * overlay.r : 1.0 - 2.0 * (1.0 - base.r) * (1.0 - overlay.r);
|
||||
result.g = overlay.g < 0.5 ? 2.0 * base.g * overlay.g : 1.0 - 2.0 * (1.0 - base.g) * (1.0 - overlay.g);
|
||||
result.b = overlay.b < 0.5 ? 2.0 * base.b * overlay.b : 1.0 - 2.0 * (1.0 - base.b) * (1.0 - overlay.b);
|
||||
return result;
|
||||
}
|
||||
|
||||
vec3 color_dodge_blend(vec3 base, vec3 overlay) {
|
||||
vec3 result;
|
||||
result.r = overlay.r >= 1.0 ? 1.0 : min(1.0, base.r / (1.0 - overlay.r));
|
||||
result.g = overlay.g >= 1.0 ? 1.0 : min(1.0, base.g / (1.0 - overlay.g));
|
||||
result.b = overlay.b >= 1.0 ? 1.0 : min(1.0, base.b / (1.0 - overlay.b));
|
||||
return result;
|
||||
}
|
||||
|
||||
vec3 color_burn_blend(vec3 base, vec3 overlay) {
|
||||
vec3 result;
|
||||
result.r = overlay.r <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.r) / overlay.r);
|
||||
result.g = overlay.g <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.g) / overlay.g);
|
||||
result.b = overlay.b <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.b) / overlay.b);
|
||||
return result;
|
||||
}
|
||||
|
||||
vec3 darken_blend(vec3 base, vec3 overlay) {
|
||||
return min(base, overlay);
|
||||
}
|
||||
|
||||
vec3 lighten_blend(vec3 base, vec3 overlay) {
|
||||
return max(base, overlay);
|
||||
}
|
||||
|
||||
vec3 add_blend(vec3 base, vec3 overlay) {
|
||||
return min(vec3(1.0), base + overlay);
|
||||
}
|
||||
|
||||
vec3 subtract_blend(vec3 base, vec3 overlay) {
|
||||
return max(vec3(0.0), base - overlay);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec4 tex_color = texture(TEXTURE, UV);
|
||||
|
||||
// Apply blend mode (only to RGB, preserve alpha channel behavior)
|
||||
vec3 final_color;
|
||||
|
||||
if (blend_mode == 0) { // Normal
|
||||
final_color = tex_color.rgb;
|
||||
} else if (blend_mode == 1) { // Multiply
|
||||
final_color = multiply_blend(vec3(1.0), tex_color.rgb);
|
||||
} else if (blend_mode == 2) { // Screen
|
||||
final_color = screen_blend(vec3(0.0), tex_color.rgb);
|
||||
} else if (blend_mode == 3) { // Overlay
|
||||
final_color = overlay_blend(vec3(0.5), tex_color.rgb);
|
||||
} else if (blend_mode == 4) { // Soft Light
|
||||
final_color = soft_light_blend(vec3(0.5), tex_color.rgb);
|
||||
} else if (blend_mode == 5) { // Hard Light
|
||||
final_color = hard_light_blend(vec3(0.5), tex_color.rgb);
|
||||
} else if (blend_mode == 6) { // Color Dodge
|
||||
final_color = color_dodge_blend(vec3(0.0), tex_color.rgb);
|
||||
} else if (blend_mode == 7) { // Color Burn
|
||||
final_color = color_burn_blend(vec3(1.0), tex_color.rgb);
|
||||
} else if (blend_mode == 8) { // Darken
|
||||
final_color = darken_blend(vec3(1.0), tex_color.rgb);
|
||||
} else if (blend_mode == 9) { // Lighten
|
||||
final_color = lighten_blend(vec3(0.0), tex_color.rgb);
|
||||
} else if (blend_mode == 10) { // Add (Linear Dodge)
|
||||
final_color = add_blend(vec3(0.0), tex_color.rgb);
|
||||
} else if (blend_mode == 11) { // Subtract
|
||||
final_color = subtract_blend(vec3(1.0), tex_color.rgb);
|
||||
} else {
|
||||
final_color = tex_color.rgb;
|
||||
}
|
||||
|
||||
COLOR = vec4(final_color, tex_color.a * opacity);
|
||||
}
|
||||
1
ui_scenes/selectedSprite/opacity_blend.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dy03x5g364v80
|
||||
@@ -6,6 +6,8 @@ var type = "sprite"
|
||||
var imageData = null
|
||||
var tex = null
|
||||
@export var path = ""
|
||||
## Custom display name for the sprite (separate from file path)
|
||||
var displayName = ""
|
||||
|
||||
var loadedImageData = null
|
||||
|
||||
@@ -28,6 +30,9 @@ var imageSize = Vector2.ZERO
|
||||
@onready var wob = $WobbleOrigin
|
||||
|
||||
@onready var outlineScene = preload("res://ui_scenes/selectedSprite/outline.tscn")
|
||||
@onready var opacityBlendShader = preload("res://ui_scenes/selectedSprite/opacity_blend.gdshader")
|
||||
|
||||
var opacityMaterial = null
|
||||
|
||||
#Visuals
|
||||
var mouseOffset = Vector2.ZERO
|
||||
@@ -76,16 +81,30 @@ var ignoreBounce = false
|
||||
#Animation
|
||||
var frames = 1
|
||||
var animSpeed = 0
|
||||
var animTime = 0.0 # Time accumulator for animations
|
||||
|
||||
var remadePolygon = false
|
||||
|
||||
var clipped = false
|
||||
|
||||
var tick = 0
|
||||
var time = 0.0 # Time accumulator for physics calculations
|
||||
|
||||
#Opacity
|
||||
var spriteOpacity = 1.0
|
||||
var affectChildrenOpacity = false
|
||||
var blendMode = 0 # Default to Normal blend mode
|
||||
|
||||
#Vis toggle
|
||||
var toggle = "null"
|
||||
|
||||
#Wobble Sync Group
|
||||
var wobbleSyncGroup = ""
|
||||
|
||||
#Transform
|
||||
var staticRotation = 0.0
|
||||
var mirrorHorizontal = false
|
||||
var mirrorVertical = false
|
||||
|
||||
func _ready():
|
||||
|
||||
Global.main.spriteVisToggles.connect(visToggle)
|
||||
@@ -163,6 +182,8 @@ func _ready():
|
||||
|
||||
setClip(clipped)
|
||||
|
||||
# Always initialize opacity shader
|
||||
updateShaderOpacity()
|
||||
|
||||
if Global.filtering:
|
||||
sprite.texture_filter = 2
|
||||
@@ -217,7 +238,7 @@ func replaceSprite(pathNew):
|
||||
remakePolygon()
|
||||
|
||||
func _process(delta):
|
||||
tick += 1
|
||||
time += delta
|
||||
if Global.heldSprite == self:
|
||||
|
||||
grabArea.visible = true
|
||||
@@ -227,6 +248,16 @@ func _process(delta):
|
||||
grabArea.visible = false
|
||||
originSprite.visible = false
|
||||
|
||||
# Disable grabArea input and detection when dialogs are open
|
||||
if Global.main and Global.main.fileSystemOpen:
|
||||
grabArea.input_pickable = false
|
||||
grabArea.monitoring = false
|
||||
grabArea.monitorable = false
|
||||
else:
|
||||
grabArea.input_pickable = true
|
||||
grabArea.monitoring = true
|
||||
grabArea.monitorable = true
|
||||
|
||||
var glob = dragger.global_position
|
||||
if ignoreBounce:
|
||||
glob.y -= Global.main.bounceChange
|
||||
@@ -238,19 +269,24 @@ func _process(delta):
|
||||
|
||||
rotationalDrag(length,delta)
|
||||
stretch(length,delta)
|
||||
applyStaticTransform()
|
||||
|
||||
if grabDelay > 0:
|
||||
grabDelay -= 1
|
||||
|
||||
talkBlink()
|
||||
|
||||
animation()
|
||||
animation(delta)
|
||||
|
||||
func animation():
|
||||
|
||||
var speed = max(float(animSpeed),Engine.max_fps*6.0)
|
||||
func animation(delta):
|
||||
if animSpeed > 0 and frames > 1:
|
||||
if Global.animationTick % int((speed)/float(animSpeed)) == 0:
|
||||
animTime += delta
|
||||
# Convert animSpeed to frames per second - no additional scaling needed since this controls frame rate
|
||||
var frameRate = animSpeed / 60.0 # Normalize from old frame-based system
|
||||
var frameInterval = 1.0 / frameRate
|
||||
|
||||
if animTime >= frameInterval:
|
||||
animTime -= frameInterval
|
||||
if sprite.frame == frames - 1:
|
||||
sprite.frame = 0
|
||||
else:
|
||||
@@ -262,12 +298,63 @@ func setZIndex():
|
||||
sprite.z_index = z
|
||||
|
||||
func talkBlink():
|
||||
var faded = 0.2 * int(Global.main.editMode)
|
||||
var faded = 0.2 * int(Global.main and Global.main.editMode)
|
||||
var value = (showOnTalk + (showOnBlink*3)) + (int(Global.speaking)*10) + (int(Global.blink)*20)
|
||||
var yes = [0,10,20,30,1,21,12,32,3,13,4,15,26,36,27,38].has(int(value))
|
||||
sprite.self_modulate.a = max(int(yes),faded)
|
||||
var baseAlpha = max(int(yes), faded)
|
||||
|
||||
# Only handle talk/blink/edit visibility with modulate
|
||||
# Opacity slider effects are handled by shader
|
||||
sprite.self_modulate.a = 1.0 # Always keep original texture alpha for clipping
|
||||
var previousAlpha = sprite.modulate.a
|
||||
sprite.modulate.a = baseAlpha # Only apply talk/blink/edit visibility
|
||||
|
||||
# Update shader if modulate alpha changed (for editor dimming)
|
||||
if previousAlpha != baseAlpha:
|
||||
updateShaderOpacity()
|
||||
|
||||
func calculateParentOpacityMultiplier():
|
||||
var multiplier = 1.0
|
||||
var currentParent = parentSprite
|
||||
|
||||
# Traverse up the entire parent hierarchy
|
||||
while currentParent != null:
|
||||
if currentParent.affectChildrenOpacity:
|
||||
multiplier *= currentParent.spriteOpacity
|
||||
currentParent = currentParent.parentSprite
|
||||
|
||||
return multiplier
|
||||
|
||||
## Update wobble parameters and sync to group if needed
|
||||
func updateWobbleParameter(parameter: String, value: float):
|
||||
|
||||
# Update this sprite's parameter first
|
||||
match parameter:
|
||||
"xFrq": xFrq = value
|
||||
"xAmp": xAmp = value
|
||||
"yFrq": yFrq = value
|
||||
"yAmp": yAmp = value
|
||||
"rFrq": rFrq = value
|
||||
"rAmp": rAmp = value
|
||||
|
||||
|
||||
# If this sprite is in a sync group, update the entire group
|
||||
if wobbleSyncGroup != "" and WobbleSyncManager:
|
||||
WobbleSyncManager.updateGroupWobble(wobbleSyncGroup, xFrq, xAmp, yFrq, yAmp, rFrq, rAmp)
|
||||
|
||||
# Don't refresh UI immediately - let the sync process complete first
|
||||
# The wobble sync control will handle UI updates when needed
|
||||
else:
|
||||
pass # Individual sprite update - no sync needed
|
||||
|
||||
## Check if this sprite is synced to a group
|
||||
func isSynced() -> bool:
|
||||
return wobbleSyncGroup != ""
|
||||
|
||||
func delete():
|
||||
# Clean up from wobble sync groups
|
||||
if wobbleSyncGroup != "" and WobbleSyncManager:
|
||||
WobbleSyncManager.onSpriteDeleted(id)
|
||||
queue_free()
|
||||
|
||||
func _physics_process(delta):
|
||||
@@ -324,31 +411,60 @@ func drag(delta):
|
||||
if dragSpeed == 0:
|
||||
dragger.global_position = wob.global_position
|
||||
else:
|
||||
dragger.global_position = lerp(dragger.global_position,wob.global_position,1/dragSpeed)
|
||||
# Convert dragSpeed to a proper delta-time lerp factor
|
||||
# Higher dragSpeed = slower movement, so we invert it
|
||||
# Use Global.main.physicsTimeMultiplier for consistent physics timing
|
||||
var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
|
||||
var lerpFactor = clamp(delta * (physicsMultiplier * 10.0 / max(dragSpeed, 0.1)), 0.0, 1.0)
|
||||
dragger.global_position = lerp(dragger.global_position, wob.global_position, lerpFactor)
|
||||
dragOrigin.global_position = dragger.global_position
|
||||
|
||||
func wobble():
|
||||
wob.position.x = sin(tick*xFrq)*xAmp
|
||||
wob.position.y = sin(tick*yFrq)*yAmp
|
||||
# Use Global.main.physicsTimeMultiplier for consistent physics timing
|
||||
var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
|
||||
var scaledTime = time * physicsMultiplier
|
||||
wob.position.x = sin(scaledTime * xFrq) * xAmp
|
||||
wob.position.y = sin(scaledTime * yFrq) * yAmp
|
||||
|
||||
func rotationalDrag(length,delta):
|
||||
func rotationalDrag(length, delta):
|
||||
var yvel = (length * rdragStr)
|
||||
|
||||
#Calculate Max angle
|
||||
yvel = clamp(yvel,rLimitMin,rLimitMax)
|
||||
yvel = clamp(yvel, rLimitMin, rLimitMax)
|
||||
|
||||
# Add rotation wobble as percentage of rotational limits range
|
||||
var rotationRange = rLimitMax - rLimitMin
|
||||
var rotationWobble = sin(tick*rFrq) * (rotationRange * rAmp * 0.01)
|
||||
var totalRotation = yvel + rotationWobble
|
||||
var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
|
||||
var scaledTime = time * physicsMultiplier
|
||||
var rotationWobble = sin(scaledTime * rFrq) * (rotationRange * rAmp * 0.01)
|
||||
var totalRotation = yvel + rotationWobble + staticRotation
|
||||
|
||||
sprite.rotation = lerp_angle(sprite.rotation,deg_to_rad(totalRotation),0.25)
|
||||
# Use delta-based lerp for smooth rotation with slower speed
|
||||
var lerpFactor = clamp(delta * (physicsMultiplier * 1.0), 0.0, 1.0) # Reduced from 15.0 to 4.0
|
||||
sprite.rotation = lerp_angle(sprite.rotation, deg_to_rad(totalRotation), lerpFactor)
|
||||
|
||||
func stretch(length,delta):
|
||||
func stretch(length, delta):
|
||||
var yvel = (length * stretchAmount * 0.01)
|
||||
var target = Vector2(1.0-yvel,1.0+yvel)
|
||||
var target = Vector2(1.0 - yvel, 1.0 + yvel)
|
||||
|
||||
sprite.scale = lerp(sprite.scale,target,0.5)
|
||||
# Use delta-based lerp for smooth stretching
|
||||
var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
|
||||
var lerpFactor = clamp(delta * (physicsMultiplier * 30.0), 0.0, 1.0)
|
||||
sprite.scale = lerp(sprite.scale, target, lerpFactor)
|
||||
|
||||
## Apply static transformation (mirroring)
|
||||
func applyStaticTransform():
|
||||
var scale_x = 1.0
|
||||
var scale_y = 1.0
|
||||
|
||||
if mirrorHorizontal:
|
||||
scale_x = -1.0
|
||||
if mirrorVertical:
|
||||
scale_y = -1.0
|
||||
|
||||
# Apply mirror scaling while preserving the stretch scale
|
||||
var current_scale = sprite.scale
|
||||
sprite.scale = Vector2(abs(current_scale.x) * scale_x, abs(current_scale.y) * scale_y)
|
||||
|
||||
func changeCollision(enable):
|
||||
grabArea.monitorable = enable
|
||||
@@ -403,6 +519,46 @@ func getAllLinkedSprites():
|
||||
linkedSprites.append(node)
|
||||
return linkedSprites
|
||||
|
||||
func updateOpacity():
|
||||
# Force a refresh of opacity for this sprite and its children
|
||||
talkBlink()
|
||||
updateShaderOpacity()
|
||||
|
||||
# Update all child sprites in the entire hierarchy if this sprite affects children
|
||||
if affectChildrenOpacity:
|
||||
updateChildrenOpacityRecursively()
|
||||
|
||||
func updateShaderOpacity():
|
||||
# Calculate total opacity from user settings and parent hierarchy
|
||||
var totalOpacity = spriteOpacity * calculateParentOpacityMultiplier()
|
||||
|
||||
# Also factor in the modulate alpha for editor dimming/onion skinning
|
||||
var editorAlpha = sprite.modulate.a
|
||||
var finalOpacity = totalOpacity * editorAlpha
|
||||
|
||||
# Always apply the opacity blend shader to ensure it's loaded
|
||||
if opacityMaterial == null:
|
||||
opacityMaterial = ShaderMaterial.new()
|
||||
opacityMaterial.shader = opacityBlendShader
|
||||
|
||||
opacityMaterial.set_shader_parameter("opacity", finalOpacity)
|
||||
opacityMaterial.set_shader_parameter("blend_mode", blendMode)
|
||||
sprite.material = opacityMaterial
|
||||
|
||||
## Update the blend mode and refresh the shader
|
||||
func updateBlendMode(newBlendMode: int):
|
||||
if BlendModeManager.is_valid_blend_mode(newBlendMode):
|
||||
blendMode = newBlendMode
|
||||
updateShaderOpacity() # Refresh shader with new blend mode
|
||||
|
||||
func updateChildrenOpacityRecursively():
|
||||
var linkedSprites = getAllLinkedSprites()
|
||||
for child in linkedSprites:
|
||||
child.talkBlink()
|
||||
child.updateShaderOpacity()
|
||||
# Always update the entire hierarchy below this child
|
||||
child.updateChildrenOpacityRecursively()
|
||||
|
||||
func visToggle(keys):
|
||||
if keys.has(toggle):
|
||||
$WobbleOrigin/DragOrigin.visible = !$WobbleOrigin/DragOrigin.visible
|
||||
|
||||
@@ -46,6 +46,11 @@ func setvalues():
|
||||
label.text = "costume " + str(tag) + " key: \"" + Global.main.costumeKeys[tag-1] + "\""
|
||||
tag += 1
|
||||
|
||||
$experimentalMicLoudness/checkmark.button_pressed = Global.experimentalMicLoudness
|
||||
|
||||
# Initialize NDI settings
|
||||
setup_ndi_settings()
|
||||
|
||||
func _on_color_picker_button_color_changed(color):
|
||||
get_viewport().transparent_bg = false
|
||||
RenderingServer.set_default_clear_color(color)
|
||||
@@ -136,13 +141,10 @@ func _on_bounce_gravity_value_changed(value):
|
||||
|
||||
func costumeButtonsPressed(label,id):
|
||||
label.text = "AWAITING INPUT"
|
||||
await Global.main.emptiedCapture
|
||||
awaitingCostumeInput = id - 1
|
||||
|
||||
|
||||
await Global.main.pressedKey
|
||||
label.text = "costume " + str(id) + " key: \"" + Global.main.costumeKeys[id - 1] + "\""
|
||||
await Global.main.emptiedCapture
|
||||
awaitingCostumeInput = -1
|
||||
|
||||
func _on_costume_button_1_pressed():
|
||||
@@ -202,10 +204,17 @@ func _on_costume_check_toggled(button_pressed):
|
||||
## @param button_pressed: bool - Whether StreamDeck should be enabled
|
||||
func _on_stream_deck_check_toggled(button_pressed):
|
||||
Saving.settings["useStreamDeck"] = button_pressed
|
||||
if ElgatoStreamDeck:
|
||||
ElgatoStreamDeck.refresh_connection()
|
||||
var streamdeck_node = get_node_or_null("/root/ElgatoStreamDeck")
|
||||
if streamdeck_node != null:
|
||||
streamdeck_node.refresh_connection()
|
||||
Global.pushUpdate("StreamDeck integration " + ("enabled" if button_pressed else "disabled") + ".")
|
||||
|
||||
## Handle experimental microphone loudness toggle
|
||||
## @param checked: bool - Whether experimental mic loudness should be enabled
|
||||
func _on_experimental_mic_loudness_toggle(checked):
|
||||
Global.experimentalMicLoudness = checked
|
||||
Saving.settings["experimentalMicLoudness"] = checked
|
||||
|
||||
|
||||
func _process(delta):
|
||||
var g = to_local(get_global_mouse_position())
|
||||
@@ -258,3 +267,86 @@ func _on_delete_9_pressed():
|
||||
func _on_delete_10_pressed():
|
||||
var label = $CostumeInputs/ScrollContainer/VBoxContainer/costumeButton10/Label
|
||||
deleteKey(label,10)
|
||||
|
||||
## NDI Output Settings Methods
|
||||
|
||||
func _on_ndi_enabled_check_toggled(toggled_on: bool):
|
||||
# Enable/disable NDI streaming by adding/removing the NDI node
|
||||
if toggled_on:
|
||||
# Enable NDI - create the node if it doesn't exist
|
||||
if not Global.main.ndi_output:
|
||||
var ndi_node = NDIOutput.new()
|
||||
ndi_node.set_name("NDIOutput") # Scene tree node name
|
||||
var source_name = $NDIOutput/ndiSourceName.text
|
||||
if source_name.is_empty():
|
||||
source_name = "PNGTuber-Plus"
|
||||
ndi_node.name = source_name # NDI source name
|
||||
ndi_node.audio_bus = StringName("") # No audio by default
|
||||
Global.main.add_child(ndi_node)
|
||||
Global.main.ndi_output = ndi_node
|
||||
print("NDI streaming enabled")
|
||||
Global.pushUpdate("NDI streaming enabled")
|
||||
update_ndi_status()
|
||||
else:
|
||||
# Disable NDI - remove the node
|
||||
if Global.main.ndi_output:
|
||||
Global.main.ndi_output.queue_free()
|
||||
Global.main.ndi_output = null
|
||||
print("NDI streaming disabled")
|
||||
Global.pushUpdate("NDI streaming disabled")
|
||||
update_ndi_status()
|
||||
|
||||
func _on_ndi_source_name_text_changed(new_text: String):
|
||||
# Update NDI source name
|
||||
if Global.main.ndi_output:
|
||||
Global.main.ndi_output.name = new_text
|
||||
print("NDI source name changed to: ", new_text)
|
||||
|
||||
func _on_ndi_audio_check_toggled(toggled_on: bool):
|
||||
# Enable/disable audio in NDI stream
|
||||
if Global.main.ndi_output:
|
||||
if toggled_on:
|
||||
Global.main.ndi_output.audio_bus = StringName("Master")
|
||||
else:
|
||||
Global.main.ndi_output.audio_bus = StringName("")
|
||||
print("NDI audio ", "enabled" if toggled_on else "disabled")
|
||||
|
||||
func _on_ndi_toggle_button_pressed():
|
||||
# Toggle NDI streaming on/off by toggling the checkbox
|
||||
var current_state = $NDIOutput/ndiEnabledCheck.button_pressed
|
||||
$NDIOutput/ndiEnabledCheck.button_pressed = !current_state
|
||||
_on_ndi_enabled_check_toggled(!current_state)
|
||||
|
||||
func update_ndi_status():
|
||||
# Update the status label and button text
|
||||
if Global.main.ndi_output:
|
||||
$NDIOutput/StatusLabel.text = "Status: Active (NDI Source: " + Global.main.ndi_output.name + ")"
|
||||
$NDIOutput/ndiEnabledCheck.button_pressed = true
|
||||
$NDIOutput/ndiSourceName.text = Global.main.ndi_output.name
|
||||
else:
|
||||
$NDIOutput/StatusLabel.text = "Status: Disabled"
|
||||
$NDIOutput/ndiEnabledCheck.button_pressed = false
|
||||
|
||||
func setup_ndi_settings():
|
||||
# Initialize NDI settings UI with current values
|
||||
if Global.main.ndi_output:
|
||||
$NDIOutput/ndiSourceName.text = Global.main.ndi_output.name
|
||||
update_ndi_status()
|
||||
|
||||
# Initialize avatar-only checkbox state
|
||||
$NDIOutput/avatarOnlyCheck.button_pressed = false
|
||||
if Global.main.ndi_avatar_viewport:
|
||||
$NDIOutput/avatarOnlyCheck.button_pressed = Global.main.ndi_avatar_viewport.visible
|
||||
|
||||
func _on_avatar_only_check_toggled(toggled_on: bool):
|
||||
# Enable/disable avatar-only NDI streaming
|
||||
if Global.main.ndi_avatar_viewport:
|
||||
Global.main.ndi_avatar_viewport.visible = toggled_on
|
||||
if toggled_on:
|
||||
# Sync avatar when enabling
|
||||
Global.main.ndi_avatar_viewport.sync_avatar()
|
||||
print("Avatar-only NDI stream enabled")
|
||||
Global.pushUpdate("Avatar-only NDI stream enabled")
|
||||
else:
|
||||
print("Avatar-only NDI stream disabled")
|
||||
Global.pushUpdate("Avatar-only NDI stream disabled")
|
||||
|
||||
@@ -14182,8 +14182,9 @@ HSlider/styles/slider = SubResource("StyleBoxFlat_t3qts")
|
||||
script = ExtResource("1_ckdhy")
|
||||
|
||||
[node name="NinePatchRect" type="NinePatchRect" parent="."]
|
||||
offset_left = -190.0
|
||||
offset_right = 419.0
|
||||
offset_bottom = 444.0
|
||||
offset_bottom = 460.0
|
||||
texture = ExtResource("1_ylauk")
|
||||
region_rect = Rect2(0, 0, 48, 48)
|
||||
patch_margin_left = 8
|
||||
@@ -14289,46 +14290,107 @@ offset_bottom = 28.0
|
||||
text = "confirm"
|
||||
|
||||
[node name="StreamDeck" type="Node2D" parent="."]
|
||||
position = Vector2(16, 373)
|
||||
position = Vector2(-173, 27)
|
||||
|
||||
[node name="Label" type="Label" parent="StreamDeck"]
|
||||
offset_right = 138.0
|
||||
offset_bottom = 34.0
|
||||
text = "use streamdeck"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="streamDeckCheck" type="CheckBox" parent="StreamDeck"]
|
||||
offset_left = 130.0
|
||||
offset_top = 1.0
|
||||
offset_right = 154.0
|
||||
offset_bottom = 25.0
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="experimentalMicLoudness" type="Node2D" parent="."]
|
||||
position = Vector2(-173, 59)
|
||||
|
||||
[node name="Label" type="Label" parent="experimentalMicLoudness"]
|
||||
offset_right = 200.0
|
||||
offset_bottom = 34.0
|
||||
text = "mic loudness"
|
||||
|
||||
[node name="checkmark" type="CheckBox" parent="experimentalMicLoudness"]
|
||||
offset_left = 130.0
|
||||
offset_top = 1.0
|
||||
offset_right = 154.0
|
||||
offset_bottom = 25.0
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="AntiAliasing" type="Node2D" parent="."]
|
||||
position = Vector2(16, 405)
|
||||
position = Vector2(-173, 91)
|
||||
|
||||
[node name="Label" type="Label" parent="AntiAliasing"]
|
||||
offset_right = 138.0
|
||||
offset_bottom = 34.0
|
||||
text = "texture filtering"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="CheckBox" type="CheckBox" parent="AntiAliasing"]
|
||||
offset_left = 130.0
|
||||
offset_top = 1.0
|
||||
offset_right = 154.0
|
||||
offset_bottom = 25.0
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="NDIOutput" type="Node2D" parent="."]
|
||||
position = Vector2(-175, 149)
|
||||
|
||||
[node name="Label" type="Label" parent="NDIOutput"]
|
||||
offset_top = 38.0
|
||||
offset_right = 138.0
|
||||
offset_bottom = 72.0
|
||||
text = "NDI streaming"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="ndiEnabledCheck" type="CheckBox" parent="NDIOutput"]
|
||||
offset_left = 130.0
|
||||
offset_top = 38.0
|
||||
offset_right = 154.0
|
||||
offset_bottom = 62.0
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="ndiSourceName" type="LineEdit" parent="NDIOutput"]
|
||||
offset_top = 64.0
|
||||
offset_right = 145.0
|
||||
offset_bottom = 95.0
|
||||
text = "PNGTuber-Plus"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="NDIOutput"]
|
||||
offset_left = 4.0
|
||||
offset_top = 100.0
|
||||
offset_right = 204.0
|
||||
offset_bottom = 123.0
|
||||
text = "Status: Not streaming"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="avatarOnlyCheck" type="CheckBox" parent="NDIOutput"]
|
||||
offset_left = 4.0
|
||||
offset_top = 125.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 149.0
|
||||
text = "Avatar-only stream"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="bounceOnCostume" type="Node2D" parent="."]
|
||||
position = Vector2(17, 341)
|
||||
position = Vector2(-172, 122)
|
||||
|
||||
[node name="Label" type="Label" parent="bounceOnCostume"]
|
||||
offset_right = 138.0
|
||||
offset_bottom = 34.0
|
||||
text = "costume bounce"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="costumeCheck" type="CheckBox" parent="bounceOnCostume"]
|
||||
offset_left = 130.0
|
||||
offset_top = 1.0
|
||||
offset_right = 154.0
|
||||
offset_bottom = 25.0
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="BounceForce" type="Node2D" parent="."]
|
||||
position = Vector2(22, 115)
|
||||
@@ -14381,13 +14443,13 @@ value = 1000.0
|
||||
scrollable = false
|
||||
|
||||
[node name="CostumeInputs" type="Node2D" parent="."]
|
||||
position = Vector2(404, 305)
|
||||
position = Vector2(49, 408)
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="CostumeInputs"]
|
||||
offset_left = -226.0
|
||||
offset_top = -78.0
|
||||
offset_right = 7.0
|
||||
offset_bottom = 123.0
|
||||
offset_top = -125.0
|
||||
offset_right = 354.0
|
||||
offset_bottom = 39.0
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CostumeInputs/ScrollContainer"]
|
||||
@@ -14824,15 +14886,8 @@ position = Vector2(-2, -2)
|
||||
texture = ExtResource("9_lywmf")
|
||||
centered = false
|
||||
|
||||
[node name="Label" type="Label" parent="CostumeInputs"]
|
||||
offset_left = -179.0
|
||||
offset_top = -109.0
|
||||
offset_right = -41.0
|
||||
offset_bottom = -75.0
|
||||
text = "costume hotkeys"
|
||||
|
||||
[node name="BlinkSpeed" type="Node2D" parent="."]
|
||||
position = Vector2(17, 283)
|
||||
position = Vector2(227, 205)
|
||||
|
||||
[node name="Label" type="Label" parent="BlinkSpeed"]
|
||||
offset_left = -10.0
|
||||
@@ -14841,6 +14896,7 @@ offset_right = 158.0
|
||||
offset_bottom = 27.0
|
||||
text = "blink speed: 1"
|
||||
horizontal_alignment = 1
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="blinkSpeed" type="HSlider" parent="BlinkSpeed"]
|
||||
offset_left = -9.0
|
||||
@@ -14851,9 +14907,10 @@ theme = SubResource("Theme_e6tc4")
|
||||
max_value = 20.0
|
||||
value = 1.0
|
||||
scrollable = false
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="BlinkChance" type="Node2D" parent="."]
|
||||
position = Vector2(17, 235)
|
||||
position = Vector2(17, 205)
|
||||
|
||||
[node name="Label" type="Label" parent="BlinkChance"]
|
||||
offset_left = -10.0
|
||||
@@ -14862,6 +14919,7 @@ offset_right = 160.0
|
||||
offset_bottom = 27.0
|
||||
text = "blink chance: 1 in 200"
|
||||
horizontal_alignment = 1
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="blinkChance" type="HSlider" parent="BlinkChance"]
|
||||
offset_left = -8.0
|
||||
@@ -14873,6 +14931,7 @@ min_value = 1.0
|
||||
max_value = 300.0
|
||||
value = 200.0
|
||||
scrollable = false
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[connection signal="pressed" from="Background/Transparent/Button" to="." method="_on_button_pressed"]
|
||||
[connection signal="pressed" from="Background/Green/greenButton" to="." method="_on_green_button_pressed"]
|
||||
@@ -14883,7 +14942,11 @@ scrollable = false
|
||||
[connection signal="value_changed" from="MaxFPS/fpsDrag" to="." method="_on_fps_drag_value_changed"]
|
||||
[connection signal="pressed" from="MaxFPS/confirm" to="." method="_on_confirm_pressed"]
|
||||
[connection signal="toggled" from="StreamDeck/streamDeckCheck" to="." method="_on_stream_deck_check_toggled"]
|
||||
[connection signal="toggled" from="experimentalMicLoudness/checkmark" to="." method="_on_experimental_mic_loudness_toggle"]
|
||||
[connection signal="toggled" from="AntiAliasing/CheckBox" to="." method="_on_check_box_toggled"]
|
||||
[connection signal="toggled" from="NDIOutput/ndiEnabledCheck" to="." method="_on_ndi_enabled_check_toggled"]
|
||||
[connection signal="text_changed" from="NDIOutput/ndiSourceName" to="." method="_on_ndi_source_name_text_changed"]
|
||||
[connection signal="toggled" from="NDIOutput/avatarOnlyCheck" to="." method="_on_avatar_only_check_toggled"]
|
||||
[connection signal="toggled" from="bounceOnCostume/costumeCheck" to="." method="_on_costume_check_toggled"]
|
||||
[connection signal="value_changed" from="BounceForce/bounceForce" to="." method="_on_bounce_force_value_changed"]
|
||||
[connection signal="value_changed" from="BounceGravity/bounceGravity" to="." method="_on_bounce_gravity_value_changed"]
|
||||
|
||||
@@ -5,15 +5,150 @@ extends Node2D
|
||||
|
||||
@onready var parentSpin = $SubViewportContainer2/SubViewport/Node3D/Sprite3D
|
||||
|
||||
@onready var spriteRotDisplay = $RotationalLimits/RotBack/SpriteDisplay
|
||||
@onready var spriteRotDisplay = $VBoxContainer/RotationalLimitsSection/RotBack/SpriteDisplay
|
||||
|
||||
|
||||
@onready var coverCollider = $Area2D/CollisionShape2D
|
||||
@onready var wobbleSyncControl = null # Will be assigned in _ready()
|
||||
@onready var blendModeDropdown = $VBoxContainer/RenderingSection/blendModeContainer/blendModeDropdown
|
||||
|
||||
## Slider management system
|
||||
var slider_manager: SliderManager = null
|
||||
|
||||
func _ready():
|
||||
Global.spriteEdit = self
|
||||
|
||||
# Initialize slider management system
|
||||
slider_manager = SliderManager.new(self)
|
||||
_setup_slider_components()
|
||||
|
||||
# Find the wobble sync control node
|
||||
wobbleSyncControl = find_child("WobbleSyncControl")
|
||||
|
||||
# Set up blend mode dropdown
|
||||
setupBlendModeDropdown()
|
||||
|
||||
## Set up all slider components
|
||||
func _setup_slider_components():
|
||||
# Initialize all the new slider components and add them to the manager
|
||||
_setup_drag_slider()
|
||||
_setup_wobble_sliders()
|
||||
_setup_rotation_sliders()
|
||||
_setup_limit_sliders()
|
||||
_setup_animation_sliders()
|
||||
_setup_opacity_slider()
|
||||
_setup_transform_sliders()
|
||||
|
||||
## Set up drag slider
|
||||
func _setup_drag_slider():
|
||||
var drag_slider = $VBoxContainer/PhysicsSection/DragSlider
|
||||
if drag_slider:
|
||||
slider_manager.add_slider(drag_slider)
|
||||
drag_slider.value_changed.connect(_on_drag_slider_value_changed)
|
||||
|
||||
## Set up wobble sliders
|
||||
func _setup_wobble_sliders():
|
||||
var wobble_sliders = [
|
||||
$VBoxContainer/WobbleSection/xFrqSlider,
|
||||
$VBoxContainer/WobbleSection/xAmpSlider,
|
||||
$VBoxContainer/WobbleSection/yFrqSlider,
|
||||
$VBoxContainer/WobbleSection/yAmpSlider,
|
||||
$VBoxContainer/WobbleSection/rFrqSlider,
|
||||
$VBoxContainer/WobbleSection/rAmpSlider
|
||||
]
|
||||
|
||||
for slider in wobble_sliders:
|
||||
if slider:
|
||||
slider_manager.add_slider(slider)
|
||||
slider.value_changed.connect(_on_wobble_slider_changed)
|
||||
|
||||
## Set up rotation sliders
|
||||
func _setup_rotation_sliders():
|
||||
var rotation_sliders = [
|
||||
$VBoxContainer/PhysicsSection/rDragSlider,
|
||||
$VBoxContainer/PhysicsSection/squashSlider
|
||||
]
|
||||
|
||||
for slider in rotation_sliders:
|
||||
if slider:
|
||||
slider_manager.add_slider(slider)
|
||||
slider.value_changed.connect(_on_rotation_slider_changed)
|
||||
|
||||
## Set up rotational limit sliders
|
||||
func _setup_limit_sliders():
|
||||
var limit_sliders = [
|
||||
$VBoxContainer/RotationalLimitsSection/rotLimitMinSlider,
|
||||
$VBoxContainer/RotationalLimitsSection/rotLimitMaxSlider
|
||||
]
|
||||
|
||||
for slider in limit_sliders:
|
||||
if slider:
|
||||
slider_manager.add_slider(slider)
|
||||
slider.value_changed.connect(_on_limit_slider_changed)
|
||||
|
||||
## Set up animation sliders
|
||||
func _setup_animation_sliders():
|
||||
var animation_sliders = [
|
||||
$VBoxContainer/AnimationSection/animFramesSlider,
|
||||
$VBoxContainer/AnimationSection/animSpeedSlider
|
||||
]
|
||||
|
||||
for slider in animation_sliders:
|
||||
if slider:
|
||||
slider_manager.add_slider(slider)
|
||||
slider.value_changed.connect(_on_animation_slider_changed)
|
||||
|
||||
## Set up opacity slider
|
||||
func _setup_opacity_slider():
|
||||
var opacity_slider = $VBoxContainer/RenderingSection/opacitySlider
|
||||
if opacity_slider:
|
||||
slider_manager.add_slider(opacity_slider)
|
||||
opacity_slider.value_changed.connect(_on_opacity_slider_changed)
|
||||
|
||||
## Set up transform sliders
|
||||
func _setup_transform_sliders():
|
||||
var rotation_slider = $VBoxContainer/TransformSection/rotationSlider
|
||||
if rotation_slider:
|
||||
slider_manager.add_slider(rotation_slider)
|
||||
rotation_slider.value_changed.connect(_on_transform_slider_changed)
|
||||
|
||||
## Generic slider change handlers
|
||||
func _on_drag_slider_value_changed(value: float):
|
||||
if Global.heldSprite:
|
||||
Global.heldSprite.dragSpeed = value
|
||||
|
||||
func _on_wobble_slider_changed(value: float):
|
||||
# Wobble sliders handle their own parameter updates
|
||||
# Just update the wobble sync control if needed
|
||||
if wobbleSyncControl:
|
||||
wobbleSyncControl.updateUI()
|
||||
|
||||
func _on_rotation_slider_changed(value: float):
|
||||
# Rotation sliders handle their own parameter updates
|
||||
if Global.heldSprite:
|
||||
changeRotLimit() # Update the rotation limit display
|
||||
|
||||
func _on_limit_slider_changed(value: float):
|
||||
# Limit sliders handle their own parameter updates
|
||||
if Global.heldSprite:
|
||||
changeRotLimit() # Update the rotation limit display
|
||||
|
||||
func _on_animation_slider_changed(value: float):
|
||||
# Animation sliders handle their own parameter updates
|
||||
if Global.heldSprite:
|
||||
# Special handling for frames slider
|
||||
var frames_slider = $VBoxContainer/AnimationSection/animFramesSlider
|
||||
if frames_slider and frames_slider.value != Global.heldSprite.frames:
|
||||
spriteSpin.hframes = Global.heldSprite.frames
|
||||
|
||||
func _on_opacity_slider_changed(value: float):
|
||||
# Opacity slider handles its own parameter updates via sprite_property
|
||||
pass
|
||||
|
||||
func _on_transform_slider_changed(value: float):
|
||||
# Transform sliders handle their own parameter updates via sprite_property
|
||||
pass
|
||||
|
||||
func setImage():
|
||||
if Global.heldSprite == null:
|
||||
return
|
||||
@@ -22,68 +157,63 @@ func setImage():
|
||||
spriteSpin.pixel_size = 1.5 / Global.heldSprite.imageData.get_size().y
|
||||
spriteSpin.hframes = Global.heldSprite.frames
|
||||
|
||||
# Apply static rotation and mirroring to 3D preview
|
||||
spriteSpin.rotation = Vector3(0, 0, deg_to_rad(Global.heldSprite.staticRotation))
|
||||
var scale_x = -1.0 if Global.heldSprite.mirrorHorizontal else 1.0
|
||||
var scale_y = -1.0 if Global.heldSprite.mirrorVertical else 1.0
|
||||
spriteSpin.scale = Vector3(scale_x, scale_y, 1.0)
|
||||
|
||||
spriteRotDisplay.texture = Global.heldSprite.tex
|
||||
spriteRotDisplay.offset = Global.heldSprite.offset
|
||||
var displaySize = Global.heldSprite.imageData.get_size().y
|
||||
spriteRotDisplay.scale = Vector2(1,1) * (150.0/displaySize)
|
||||
|
||||
$Slider/Label.text = "drag: " + str(Global.heldSprite.dragSpeed)
|
||||
$Slider/DragSlider.value = Global.heldSprite.dragSpeed
|
||||
# Update all slider components with the current sprite
|
||||
if slider_manager:
|
||||
slider_manager.update_all_sliders(Global.heldSprite)
|
||||
|
||||
# Update individual sliders with current values
|
||||
_update_all_slider_values()
|
||||
|
||||
$WobbleControl/xFrqLabel.text = "x frequency: " + str(Global.heldSprite.xFrq)
|
||||
$WobbleControl/xAmpLabel.text = "x amplitude: " + str(Global.heldSprite.xAmp)
|
||||
$Position/fileTitle.text = Global.heldSprite.path.replace("user://", "")
|
||||
|
||||
$WobbleControl/xFrq.value = Global.heldSprite.xFrq
|
||||
$WobbleControl/xAmp.value = Global.heldSprite.xAmp
|
||||
|
||||
$WobbleControl/yFrqLabel.text = "y frequency: " + str(Global.heldSprite.yFrq)
|
||||
$WobbleControl/yAmpLabel.text = "y amplitude: " + str(Global.heldSprite.yAmp)
|
||||
|
||||
$WobbleControl/yFrq.value = Global.heldSprite.yFrq
|
||||
$WobbleControl/yAmp.value = Global.heldSprite.yAmp
|
||||
$VBoxContainer/PhysicsSection/BounceVelocity.button_pressed = Global.heldSprite.ignoreBounce
|
||||
$VBoxContainer/RenderingSection/ClipLinked.button_pressed = Global.heldSprite.clipped
|
||||
|
||||
$WobbleControl/rFrqLabel.text = "rotation frequency: " + str(Global.heldSprite.rFrq)
|
||||
$WobbleControl/rAmpLabel.text = "rotation percentage: " + str(Global.heldSprite.rAmp) + "%"
|
||||
|
||||
$WobbleControl/rFrq.value = Global.heldSprite.rFrq
|
||||
$WobbleControl/rAmp.value = Global.heldSprite.rAmp
|
||||
$VBoxContainer/Buttons/SpeakingWrapper/Speaking.frame = Global.heldSprite.showOnTalk
|
||||
$VBoxContainer/Buttons/BlinkingWrapper/Blinking.frame = Global.heldSprite.showOnBlink
|
||||
|
||||
$Rotation/rDragLabel.text = "rotational drag: " + str(Global.heldSprite.rdragStr)
|
||||
$Rotation/rDrag.value = Global.heldSprite.rdragStr
|
||||
$VBoxContainer/RenderingSection/affectChildrenCheck.button_pressed = Global.heldSprite.affectChildrenOpacity
|
||||
$VBoxContainer/RenderingSection/blendModeContainer/blendModeDropdown.selected = Global.heldSprite.blendMode
|
||||
|
||||
$Buttons/Speaking.frame = Global.heldSprite.showOnTalk
|
||||
$Buttons/Blinking.frame = Global.heldSprite.showOnBlink
|
||||
# Update transform controls
|
||||
$VBoxContainer/TransformSection/MirrorControls/mirrorHorizontalCheck.button_pressed = Global.heldSprite.mirrorHorizontal
|
||||
$VBoxContainer/TransformSection/MirrorControls/mirrorVerticalCheck.button_pressed = Global.heldSprite.mirrorVertical
|
||||
|
||||
$RotationalLimits/rotLimitMin.value = Global.heldSprite.rLimitMin
|
||||
$RotationalLimits/RotLimitMin.text = "rotational limit min: " + str(Global.heldSprite.rLimitMin)
|
||||
$RotationalLimits/rotLimitMax.value = Global.heldSprite.rLimitMax
|
||||
$RotationalLimits/RotLimitMax.text = "rotational limit max: " + str(Global.heldSprite.rLimitMax)
|
||||
|
||||
$Rotation/squashlabel.text = "squash: " + str(Global.heldSprite.stretchAmount)
|
||||
$Rotation/squash.value = Global.heldSprite.stretchAmount
|
||||
|
||||
$Position/fileTitle.text = Global.heldSprite.path
|
||||
|
||||
$Buttons/CheckBox.button_pressed = Global.heldSprite.ignoreBounce
|
||||
$Buttons/ClipLinked.button_pressed = Global.heldSprite.clipped
|
||||
|
||||
$Animation/animSpeedLabel.text = "animation speed: " + str(Global.heldSprite.animSpeed)
|
||||
$Animation/animSpeed.value = Global.heldSprite.animSpeed
|
||||
|
||||
$Animation/animFramesLabel.text = "sprite frames: " + str(Global.heldSprite.frames)
|
||||
$Animation/animFrames.value = Global.heldSprite.frames
|
||||
|
||||
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
|
||||
$VBoxContainer/BoxContainer/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
|
||||
|
||||
changeRotLimit()
|
||||
|
||||
setLayerButtons()
|
||||
|
||||
# Update wobble sync control
|
||||
if wobbleSyncControl:
|
||||
wobbleSyncControl.setSprite(Global.heldSprite)
|
||||
# Force a refresh to ensure dropdown is populated
|
||||
await get_tree().process_frame
|
||||
wobbleSyncControl.updateUI()
|
||||
|
||||
# Update wobble control states based on sync status
|
||||
updateWobbleControlStates()
|
||||
|
||||
if Global.heldSprite.parentId == null:
|
||||
$Buttons/Unlink.visible = false
|
||||
$VBoxContainer/Buttons/UnlinkWrapper/Unlink.visible = false
|
||||
parentSpin.visible = false
|
||||
else:
|
||||
$Buttons/Unlink.visible = true
|
||||
$VBoxContainer/Buttons/UnlinkWrapper/Unlink.visible = true
|
||||
|
||||
var nodes = get_tree().get_nodes_in_group(str(Global.heldSprite.parentId))
|
||||
|
||||
@@ -94,6 +224,97 @@ func setImage():
|
||||
parentSpin.pixel_size = 1.5 / nodes[0].imageData.get_size().y
|
||||
parentSpin.hframes = nodes[0].frames
|
||||
parentSpin.visible = true
|
||||
|
||||
## Update all slider components with current sprite values
|
||||
func _update_all_slider_values():
|
||||
if not Global.heldSprite:
|
||||
return
|
||||
|
||||
# Update drag slider
|
||||
var drag_slider = $VBoxContainer/PhysicsSection/DragSlider
|
||||
if drag_slider:
|
||||
drag_slider.set_value(Global.heldSprite.dragSpeed)
|
||||
|
||||
# Update wobble sliders
|
||||
var wobble_sliders = {
|
||||
$VBoxContainer/WobbleSection/xFrqSlider: Global.heldSprite.xFrq,
|
||||
$VBoxContainer/WobbleSection/xAmpSlider: Global.heldSprite.xAmp,
|
||||
$VBoxContainer/WobbleSection/yFrqSlider: Global.heldSprite.yFrq,
|
||||
$VBoxContainer/WobbleSection/yAmpSlider: Global.heldSprite.yAmp,
|
||||
$VBoxContainer/WobbleSection/rFrqSlider: Global.heldSprite.rFrq,
|
||||
$VBoxContainer/WobbleSection/rAmpSlider: Global.heldSprite.rAmp
|
||||
}
|
||||
|
||||
for slider in wobble_sliders:
|
||||
if slider:
|
||||
slider.set_value(wobble_sliders[slider])
|
||||
|
||||
# Update rotation sliders
|
||||
var rotation_sliders = {
|
||||
$VBoxContainer/PhysicsSection/rDragSlider: Global.heldSprite.rdragStr,
|
||||
$VBoxContainer/PhysicsSection/squashSlider: Global.heldSprite.stretchAmount
|
||||
}
|
||||
|
||||
for slider in rotation_sliders:
|
||||
if slider:
|
||||
slider.set_value(rotation_sliders[slider])
|
||||
|
||||
# Update limit sliders
|
||||
var limit_sliders = {
|
||||
$VBoxContainer/RotationalLimitsSection/rotLimitMinSlider: Global.heldSprite.rLimitMin,
|
||||
$VBoxContainer/RotationalLimitsSection/rotLimitMaxSlider: Global.heldSprite.rLimitMax
|
||||
}
|
||||
|
||||
for slider in limit_sliders:
|
||||
if slider:
|
||||
slider.set_value(limit_sliders[slider])
|
||||
|
||||
# Update animation sliders
|
||||
var animation_sliders = {
|
||||
$VBoxContainer/AnimationSection/animFramesSlider: Global.heldSprite.frames,
|
||||
$VBoxContainer/AnimationSection/animSpeedSlider: Global.heldSprite.animSpeed
|
||||
}
|
||||
|
||||
for slider in animation_sliders:
|
||||
if slider:
|
||||
slider.set_value(animation_sliders[slider])
|
||||
|
||||
# Update opacity slider
|
||||
var opacity_slider = $VBoxContainer/RenderingSection/opacitySlider
|
||||
if opacity_slider:
|
||||
opacity_slider.set_value(Global.heldSprite.spriteOpacity)
|
||||
|
||||
# Update transform slider
|
||||
var rotation_slider = $VBoxContainer/TransformSection/rotationSlider
|
||||
if rotation_slider:
|
||||
rotation_slider.set_value(Global.heldSprite.staticRotation)
|
||||
|
||||
## Update wobble control states based on sync status
|
||||
func updateWobbleControlStates():
|
||||
if Global.heldSprite == null:
|
||||
return
|
||||
|
||||
# Use slider manager if available
|
||||
if slider_manager:
|
||||
slider_manager.update_wobble_control_states(Global.heldSprite)
|
||||
|
||||
# Also update individual wobble sliders for visual feedback
|
||||
var is_synced = Global.heldSprite.isSynced()
|
||||
var tint = Color(1.0, 1.0, 0.9) if is_synced else Color.WHITE
|
||||
|
||||
var wobble_sliders = [
|
||||
$VBoxContainer/WobbleSection/xFrqSlider,
|
||||
$VBoxContainer/WobbleSection/xAmpSlider,
|
||||
$VBoxContainer/WobbleSection/yFrqSlider,
|
||||
$VBoxContainer/WobbleSection/yAmpSlider,
|
||||
$VBoxContainer/WobbleSection/rFrqSlider,
|
||||
$VBoxContainer/WobbleSection/rAmpSlider
|
||||
]
|
||||
|
||||
for slider in wobble_sliders:
|
||||
if slider and slider.has_method("set_modulate_color"):
|
||||
slider.set_modulate_color(tint)
|
||||
slider.set_editable(true) # Keep enabled even when synced
|
||||
|
||||
func _process(delta):
|
||||
|
||||
@@ -107,9 +328,9 @@ func _process(delta):
|
||||
spriteSpin.rotate_y(delta*4.0)
|
||||
parentSpin.rotate_y(delta*4.0)
|
||||
|
||||
$Position/Label.text = "position X : "+str(obj.position.x)+" Y: " + str(obj.position.y)
|
||||
$Position/Label2.text = "offset X : "+str(obj.offset.x)+" Y: " + str(obj.offset.y)
|
||||
$Position/Label3.text = "layer : "+str(obj.z)
|
||||
$VBoxContainer/InfoSection/PositionLabel.text = "position X : "+str(obj.position.x)+" Y: " + str(obj.position.y)
|
||||
$VBoxContainer/InfoSection/OffsetLabel.text = "offset X : "+str(obj.offset.x)+" Y: " + str(obj.offset.y)
|
||||
$VBoxContainer/InfoSection/LayerLabel.text = "layer : "+str(obj.z)
|
||||
|
||||
#Sprite Rotational Limit Display
|
||||
|
||||
@@ -117,65 +338,30 @@ func _process(delta):
|
||||
var minimum = Global.heldSprite.rLimitMin
|
||||
|
||||
spriteRotDisplay.rotation_degrees = sin(Global.animationTick*0.05)*(size/2.0)+(minimum+(size/2.0))
|
||||
$RotationalLimits/RotBack/RotLineDisplay3.rotation_degrees = spriteRotDisplay.rotation_degrees
|
||||
|
||||
|
||||
func _on_drag_slider_value_changed(value):
|
||||
if Global.heldSprite != null:
|
||||
$Slider/Label.text = "drag: " + str(value)
|
||||
Global.heldSprite.dragSpeed = value
|
||||
|
||||
|
||||
func _on_x_frq_value_changed(value):
|
||||
$WobbleControl/xFrqLabel.text = "x frequency: " + str(value)
|
||||
Global.heldSprite.xFrq = value
|
||||
|
||||
|
||||
func _on_x_amp_value_changed(value):
|
||||
$WobbleControl/xAmpLabel.text = "x amplitude: " + str(value)
|
||||
Global.heldSprite.xAmp = value
|
||||
|
||||
|
||||
func _on_y_frq_value_changed(value):
|
||||
$WobbleControl/yFrqLabel.text = "y frequency: " + str(value)
|
||||
Global.heldSprite.yFrq = value
|
||||
|
||||
func _on_y_amp_value_changed(value):
|
||||
$WobbleControl/yAmpLabel.text = "y amplitude: " + str(value)
|
||||
Global.heldSprite.yAmp = value
|
||||
|
||||
|
||||
func _on_r_frq_value_changed(value):
|
||||
$WobbleControl/rFrqLabel.text = "rotation frequency: " + str(value)
|
||||
Global.heldSprite.rFrq = value
|
||||
|
||||
func _on_r_amp_value_changed(value):
|
||||
$WobbleControl/rAmpLabel.text = "rotation amplitude: " + str(value) + "%"
|
||||
Global.heldSprite.rAmp = value
|
||||
|
||||
|
||||
func _on_r_drag_value_changed(value):
|
||||
$Rotation/rDragLabel.text = "rotational drag: " + str(value)
|
||||
Global.heldSprite.rdragStr = value
|
||||
$VBoxContainer/RotationalLimitsSection/RotBack/RotLineDisplay3.rotation_degrees = spriteRotDisplay.rotation_degrees
|
||||
|
||||
|
||||
func _on_speaking_pressed():
|
||||
var f = $Buttons/Speaking.frame
|
||||
var f = $VBoxContainer/Buttons/SpeakingWrapper/Speaking.frame
|
||||
f = (f+1) % 3
|
||||
|
||||
$Buttons/Speaking.frame = f
|
||||
$VBoxContainer/Buttons/SpeakingWrapper/Speakingaking.frame = f
|
||||
Global.heldSprite.showOnTalk = f
|
||||
|
||||
|
||||
func _on_blinking_pressed():
|
||||
var f = $Buttons/Blinking.frame
|
||||
var f = $VBoxContainer/Buttons/BlinkingWrapper/Blinking.frame
|
||||
f = (f+1) % 3
|
||||
|
||||
$Buttons/Blinking.frame = f
|
||||
$VBoxContainer/Buttons/BlinkingWrapper/Blinking.frame = f
|
||||
Global.heldSprite.showOnBlink = f
|
||||
|
||||
|
||||
func _on_trash_pressed():
|
||||
# Clean up wobble sync group membership before deleting
|
||||
if Global.heldSprite.wobbleSyncGroup != "" and WobbleSyncManager:
|
||||
WobbleSyncManager.onSpriteDeleted(Global.heldSprite.id)
|
||||
|
||||
Global.heldSprite.queue_free()
|
||||
Global.heldSprite = null
|
||||
|
||||
@@ -188,39 +374,34 @@ func _on_unlink_pressed():
|
||||
setImage()
|
||||
|
||||
|
||||
func _on_rot_limit_min_value_changed(value):
|
||||
$RotationalLimits/RotLimitMin.text = "rotational limit min: " + str(value)
|
||||
Global.heldSprite.rLimitMin = value
|
||||
|
||||
changeRotLimit()
|
||||
|
||||
func _on_rot_limit_max_value_changed(value):
|
||||
$RotationalLimits/RotLimitMax.text = "rotational limit max: " + str(value)
|
||||
Global.heldSprite.rLimitMax = value
|
||||
|
||||
changeRotLimit()
|
||||
|
||||
func changeRotLimit():
|
||||
$RotationalLimits/RotBack/rotLimitBar.value = Global.heldSprite.rLimitMax - Global.heldSprite.rLimitMin
|
||||
$RotationalLimits/RotBack/rotLimitBar.rotation_degrees = Global.heldSprite.rLimitMin + 90
|
||||
$VBoxContainer/RotationalLimitsSection/RotBack/rotLimitBar.value = Global.heldSprite.rLimitMax - Global.heldSprite.rLimitMin
|
||||
$VBoxContainer/RotationalLimitsSection/RotBack/rotLimitBar.rotation_degrees = Global.heldSprite.rLimitMin + 90
|
||||
|
||||
$VBoxContainer/RotationalLimitsSection/RotBack/RotLineDisplay.rotation_degrees = Global.heldSprite.rLimitMin
|
||||
$VBoxContainer/RotationalLimitsSection/RotBack/RotLineDisplay2.rotation_degrees = Global.heldSprite.rLimitMax
|
||||
|
||||
$RotationalLimits/RotBack/RotLineDisplay.rotation_degrees = Global.heldSprite.rLimitMin
|
||||
$RotationalLimits/RotBack/RotLineDisplay2.rotation_degrees = Global.heldSprite.rLimitMax
|
||||
|
||||
func setLayerButtons():
|
||||
if Global.heldSprite == null:
|
||||
return
|
||||
|
||||
var a = Global.heldSprite.costumeLayers.duplicate()
|
||||
|
||||
$Layers/Layer1.frame = 1-a[0]
|
||||
$Layers/Layer2.frame = 1-a[1]
|
||||
$Layers/Layer3.frame = 1-a[2]
|
||||
$Layers/Layer4.frame = 1-a[3]
|
||||
$Layers/Layer5.frame = 1-a[4]
|
||||
$Layers/Layer6.frame = 1-a[5]
|
||||
$Layers/Layer7.frame = 1-a[6]
|
||||
$Layers/Layer8.frame = 1-a[7]
|
||||
$Layers/Layer9.frame = 1-a[8]
|
||||
$Layers/Layer10.frame = 1-a[9]
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer1Container/Layer1.frame = 1-a[0]
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer2Container/Layer2.frame = 1-a[1]
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer3Container/Layer3.frame = 1-a[2]
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer4Container/Layer4.frame = 1-a[3]
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer5Container/Layer5.frame = 1-a[4]
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer6Container/Layer6.frame = 1-a[5]
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer7Container/Layer7.frame = 1-a[6]
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer8Container/Layer8.frame = 1-a[7]
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer9Container/Layer9.frame = 1-a[8]
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer10Container/Layer10.frame = 1-a[9]
|
||||
|
||||
# Update checkmarks to show current costume layer
|
||||
updateLayerCheckmarks()
|
||||
|
||||
# Update sprite visibility for all sprites based on current costume
|
||||
var nodes = get_tree().get_nodes_in_group("saved")
|
||||
for sprite in nodes:
|
||||
if sprite.costumeLayers[Global.main.costume - 1] == 1:
|
||||
@@ -229,7 +410,45 @@ func setLayerButtons():
|
||||
else:
|
||||
sprite.visible = false
|
||||
sprite.changeCollision(false)
|
||||
|
||||
|
||||
# Update the sprite list to reflect visibility changes
|
||||
Global.spriteList.updateAllVisible()
|
||||
|
||||
func updateLayerCheckmarks():
|
||||
# Hide all checkmarks first
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer1Container/Layer1/Checkmark1.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer2Container/Layer2/Checkmark2.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer3Container/Layer3/Checkmark3.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer4Container/Layer4/Checkmark4.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer5Container/Layer5/Checkmark5.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer6Container/Layer6/Checkmark6.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer7Container/Layer7/Checkmark7.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer8Container/Layer8/Checkmark8.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer9Container/Layer9/Checkmark9.visible = false
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer10Container/Layer10/Checkmark10.visible = false
|
||||
|
||||
# Show checkmark for current costume layer
|
||||
match Global.main.costume:
|
||||
1:
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer1Container/Layer1/Checkmark1.visible = true
|
||||
2:
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer2Container/Layer2/Checkmark2.visible = true
|
||||
3:
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer3Container/Layer3/Checkmark3.visible = true
|
||||
4:
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer4Container/Layer4/Checkmark4.visible = true
|
||||
5:
|
||||
$VBoxContainer/LayersSection/HBoxContainer/Layer5Container/Layer5/Checkmark5.visible = true
|
||||
6:
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer6Container/Layer6/Checkmark6.visible = true
|
||||
7:
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer7Container/Layer7/Checkmark7.visible = true
|
||||
8:
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer8Container/Layer8/Checkmark8.visible = true
|
||||
9:
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer9Container/Layer9/Checkmark9.visible = true
|
||||
10:
|
||||
$VBoxContainer/LayersSection/HBoxContainer2/Layer10Container/Layer10/Checkmark10.visible = true
|
||||
|
||||
|
||||
func _on_layer_button_1_pressed():
|
||||
@@ -310,62 +529,73 @@ func layerSelected():
|
||||
var newPos = Vector2.ZERO
|
||||
match Global.main.costume:
|
||||
1:
|
||||
newPos = $Layers/Layer1.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer1Container/Layer1.position
|
||||
2:
|
||||
newPos = $Layers/Layer2.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer2Container/Layer2.position
|
||||
3:
|
||||
newPos = $Layers/Layer3.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer3Container/Layer3.position
|
||||
4:
|
||||
newPos = $Layers/Layer4.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer4Container/Layer4.position
|
||||
5:
|
||||
newPos = $Layers/Layer5.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer5Container/Layer5.position
|
||||
6:
|
||||
newPos = $Layers/Layer6.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer6Container/Layer6.position
|
||||
7:
|
||||
newPos = $Layers/Layer7.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer7Container/Layer7.position
|
||||
8:
|
||||
newPos = $Layers/Layer8.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer8Container/Layer8.position
|
||||
9:
|
||||
newPos = $Layers/Layer9.position
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer9Container/Layer9.position
|
||||
10:
|
||||
newPos = $Layers/Layer10.position
|
||||
$Layers/Select.position = newPos
|
||||
|
||||
|
||||
func _on_squash_value_changed(value):
|
||||
$Rotation/squashlabel.text = "squash: " + str(value)
|
||||
Global.heldSprite.stretchAmount = value
|
||||
|
||||
|
||||
func _on_check_box_toggled(button_pressed):
|
||||
Global.heldSprite.ignoreBounce = button_pressed
|
||||
|
||||
|
||||
func _on_anim_speed_value_changed(value):
|
||||
$Animation/animSpeedLabel.text = "animation speed: " + str(value)
|
||||
Global.heldSprite.animSpeed = value
|
||||
|
||||
func _on_anim_frames_value_changed(value):
|
||||
$Animation/animFramesLabel.text = "sprite frames: " + str(value)
|
||||
Global.heldSprite.frames = value
|
||||
spriteSpin.hframes = Global.heldSprite.frames
|
||||
Global.heldSprite.changeFrames()
|
||||
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer10Container/Layer10.position
|
||||
|
||||
# Update selection indicator position if it exists
|
||||
var select_node = get_node_or_null("VBoxContainer/LayersSection/Select")
|
||||
if select_node:
|
||||
select_node.position = newPos
|
||||
|
||||
|
||||
func _on_clip_linked_toggled(button_pressed):
|
||||
Global.heldSprite.setClip(button_pressed)
|
||||
|
||||
|
||||
func _on_check_box_toggled(button_pressed):
|
||||
Global.heldSprite.ignoreBounce = button_pressed
|
||||
|
||||
|
||||
func _on_delete_pressed():
|
||||
Global.heldSprite.toggle = "null"
|
||||
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
|
||||
$VBoxContainer/BoxContainer/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
|
||||
Global.heldSprite.makeVis()
|
||||
|
||||
func _on_set_toggle_pressed():
|
||||
$VisToggle/setToggle/Label.text = "toggle: AWAITING INPUT"
|
||||
$VBoxContainer/BoxContainer/setToggle/Label.text = "toggle: AWAITING INPUT"
|
||||
await Global.main.fatfuckingballs
|
||||
|
||||
var keys = await Global.main.spriteVisToggles
|
||||
var key = keys[0]
|
||||
Global.heldSprite.toggle = key
|
||||
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
|
||||
$VBoxContainer/BoxContainer/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
|
||||
|
||||
func _on_affect_children_check_toggled(button_pressed):
|
||||
Global.heldSprite.affectChildrenOpacity = button_pressed
|
||||
Global.heldSprite.updateOpacity()
|
||||
|
||||
func _on_mirror_horizontal_check_toggled(button_pressed):
|
||||
Global.heldSprite.mirrorHorizontal = button_pressed
|
||||
|
||||
func _on_mirror_vertical_check_toggled(button_pressed):
|
||||
Global.heldSprite.mirrorVertical = button_pressed
|
||||
|
||||
## Set up the blend mode dropdown with all available blend modes
|
||||
func setupBlendModeDropdown():
|
||||
blendModeDropdown.clear()
|
||||
var blendModeNames = BlendModeManager.get_blend_mode_names()
|
||||
for mode_name in blendModeNames:
|
||||
blendModeDropdown.add_item(mode_name)
|
||||
|
||||
## Handle blend mode dropdown selection
|
||||
func _on_blend_mode_dropdown_item_selected(index: int):
|
||||
if Global.heldSprite != null:
|
||||
Global.heldSprite.updateBlendMode(index)
|
||||
Global.pushUpdate("Blend mode set to: " + BlendModeManager.get_blend_mode_name(index))
|
||||
|
||||
15017
ui_scenes/spriteEditMenu/sprite_viewer.tscn19865666400.tmp
Normal file
190
ui_scenes/spriteEditMenu/wobble_sync_control.gd
Normal file
@@ -0,0 +1,190 @@
|
||||
extends Control
|
||||
|
||||
## WobbleSyncControl - UI component for managing wobble sync groups
|
||||
## Designed to integrate into the sprite editor panel
|
||||
|
||||
@onready var groupDropdown = $VBoxContainer/GroupRow/GroupDropdown
|
||||
@onready var syncIndicator = $VBoxContainer/SyncIndicator
|
||||
@onready var createGroupDialog = $CreateGroupDialog
|
||||
@onready var groupNameInput = $CreateGroupDialog/VBoxContainer/LineEdit
|
||||
|
||||
var currentSprite = null
|
||||
var updating_dropdown = false # Flag to prevent recursive calls
|
||||
|
||||
signal group_changed(sprite_id: int, group_name: String)
|
||||
|
||||
func _ready():
|
||||
# Wait for autoload to be ready
|
||||
await get_tree().process_frame
|
||||
|
||||
# Connect UI signals - no add button to connect
|
||||
groupDropdown.item_selected.connect(_on_group_dropdown_selected)
|
||||
createGroupDialog.confirmed.connect(_on_create_group_confirmed)
|
||||
createGroupDialog.canceled.connect(_on_create_group_cancelled)
|
||||
|
||||
# Connect to wobble sync manager signals
|
||||
if WobbleSyncManager:
|
||||
WobbleSyncManager.group_created.connect(_on_group_created)
|
||||
WobbleSyncManager.group_deleted.connect(_on_group_deleted)
|
||||
|
||||
# Setup create group dialog
|
||||
createGroupDialog.title = "Create Wobble Sync Group"
|
||||
createGroupDialog.size = Vector2(300, 150)
|
||||
createGroupDialog.unresizable = false
|
||||
createGroupDialog.transient = true # Prevent click-through
|
||||
createGroupDialog.exclusive = true # Make modal
|
||||
createGroupDialog.popup_window = false # Keep as embedded dialog
|
||||
createGroupDialog.always_on_top = true
|
||||
|
||||
updateUI()
|
||||
|
||||
## Update the UI based on current sprite selection
|
||||
func updateUI():
|
||||
updateGroupDropdown()
|
||||
updateSyncIndicator()
|
||||
|
||||
## Update the group dropdown with available options
|
||||
func updateGroupDropdown():
|
||||
updating_dropdown = true # Prevent recursive calls
|
||||
|
||||
groupDropdown.clear()
|
||||
|
||||
# Add "None" option
|
||||
groupDropdown.add_item("None")
|
||||
|
||||
# Add existing groups
|
||||
if WobbleSyncManager:
|
||||
var groups = WobbleSyncManager.getGroupNames()
|
||||
for group_name in groups:
|
||||
groupDropdown.add_item(group_name)
|
||||
else:
|
||||
pass # WobbleSyncManager not available
|
||||
|
||||
# Add "Create New..." option
|
||||
groupDropdown.add_separator()
|
||||
groupDropdown.add_item("Create New...")
|
||||
|
||||
# Select current group if sprite is synced
|
||||
if currentSprite != null and currentSprite.wobbleSyncGroup != "":
|
||||
var group_index = -1
|
||||
for i in range(groupDropdown.get_item_count()):
|
||||
if groupDropdown.get_item_text(i) == currentSprite.wobbleSyncGroup:
|
||||
group_index = i
|
||||
break
|
||||
|
||||
if group_index != -1:
|
||||
groupDropdown.selected = group_index
|
||||
else:
|
||||
groupDropdown.selected = 0 # Select "None"
|
||||
else:
|
||||
groupDropdown.selected = 0 # Select "None"
|
||||
|
||||
updating_dropdown = false
|
||||
|
||||
## Update the sync indicator
|
||||
func updateSyncIndicator():
|
||||
if currentSprite == null:
|
||||
syncIndicator.visible = false
|
||||
return
|
||||
|
||||
if currentSprite.isSynced():
|
||||
syncIndicator.visible = true
|
||||
syncIndicator.text = "⚡ Synced to \"" + currentSprite.wobbleSyncGroup + "\""
|
||||
else:
|
||||
syncIndicator.visible = false
|
||||
|
||||
## Set the current sprite to manage
|
||||
func setSprite(sprite):
|
||||
currentSprite = sprite
|
||||
if currentSprite:
|
||||
pass # Sprite is valid
|
||||
updateUI()
|
||||
|
||||
## Get available groups for the dropdown (excluding "None" and "Create New...")
|
||||
func getAvailableGroups() -> Array:
|
||||
var groups = []
|
||||
for i in range(1, groupDropdown.get_item_count() - 2): # Skip "None" and "Create New..."
|
||||
groups.append(groupDropdown.get_item_text(i))
|
||||
return groups
|
||||
|
||||
## Handle group dropdown selection
|
||||
func _on_group_dropdown_selected(index: int):
|
||||
if currentSprite == null or updating_dropdown:
|
||||
return
|
||||
|
||||
var selected_text = groupDropdown.get_item_text(index)
|
||||
|
||||
match selected_text:
|
||||
"None":
|
||||
# Remove sprite from any group
|
||||
if currentSprite.wobbleSyncGroup != "" and WobbleSyncManager:
|
||||
WobbleSyncManager.removeSpriteFromAllGroups(currentSprite.id)
|
||||
Global.pushUpdate("Removed sprite from wobble sync group")
|
||||
|
||||
"Create New...":
|
||||
# Show create group dialog
|
||||
groupNameInput.text = ""
|
||||
groupNameInput.placeholder_text = "Enter group name..."
|
||||
createGroupDialog.popup_centered_clamped(Vector2i(300, 150))
|
||||
groupNameInput.grab_focus()
|
||||
# Reset dropdown to current selection
|
||||
updateGroupDropdown()
|
||||
|
||||
_:
|
||||
# Join selected group
|
||||
if WobbleSyncManager and WobbleSyncManager.hasGroup(selected_text):
|
||||
WobbleSyncManager.addSpriteToGroup(currentSprite.id, selected_text)
|
||||
# Refresh sprite editor to show updated wobble values
|
||||
if Global.spriteEdit:
|
||||
Global.spriteEdit.setImage()
|
||||
else:
|
||||
pass # Group doesn't exist
|
||||
|
||||
updateSyncIndicator()
|
||||
|
||||
## Handle create group dialog confirmation
|
||||
func _on_create_group_confirmed():
|
||||
var group_name = groupNameInput.text.strip_edges()
|
||||
|
||||
if group_name == "":
|
||||
Global.pushUpdate("Group name cannot be empty")
|
||||
return
|
||||
|
||||
if WobbleSyncManager and WobbleSyncManager.hasGroup(group_name):
|
||||
Global.pushUpdate("Group \"" + group_name + "\" already exists")
|
||||
return
|
||||
|
||||
# Create group with current sprite
|
||||
if currentSprite != null and WobbleSyncManager:
|
||||
if WobbleSyncManager.createGroup(group_name, currentSprite.id):
|
||||
createGroupDialog.hide()
|
||||
updateUI()
|
||||
# Refresh sprite editor to show sync status
|
||||
if Global.spriteEdit:
|
||||
Global.spriteEdit.setImage()
|
||||
else:
|
||||
pass # Failed to create group
|
||||
else:
|
||||
pass # No sprite or WobbleSyncManager not available
|
||||
|
||||
## Handle create group dialog cancellation
|
||||
func _on_create_group_cancelled():
|
||||
createGroupDialog.hide()
|
||||
updateGroupDropdown() # Reset dropdown to current selection
|
||||
|
||||
## Handle group creation signal from manager
|
||||
func _on_group_created(group_name: String):
|
||||
updateGroupDropdown()
|
||||
|
||||
## Handle group deletion signal from manager
|
||||
func _on_group_deleted(group_name: String):
|
||||
updateGroupDropdown()
|
||||
updateSyncIndicator()
|
||||
|
||||
## Check if current sprite can have wobble syncing applied
|
||||
func canSync() -> bool:
|
||||
return currentSprite != null
|
||||
|
||||
## Force refresh the UI - useful after loading saves
|
||||
func forceRefresh():
|
||||
updateUI()
|
||||
1
ui_scenes/spriteEditMenu/wobble_sync_control.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ykov43vxml0k
|
||||
59
ui_scenes/spriteEditMenu/wobble_sync_control.tscn
Normal file
@@ -0,0 +1,59 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bkwf3j2pdspxw"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://ykov43vxml0k" path="res://ui_scenes/spriteEditMenu/wobble_sync_control.gd" id="1_k8h4v"]
|
||||
|
||||
[node name="WobbleSyncControl" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_k8h4v")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="GroupRow" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GroupLabel" type="Label" parent="VBoxContainer/GroupRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
text = "Group:"
|
||||
|
||||
[node name="GroupDropdown" type="OptionButton" parent="VBoxContainer/GroupRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="SyncIndicator" type="Label" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "⚡ Synced to \"Group Name\""
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="CreateGroupDialog" type="ConfirmationDialog" parent="."]
|
||||
process_mode = 3
|
||||
title = "Create Wobble Sync Group"
|
||||
size = Vector2i(300, 150)
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CreateGroupDialog"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = -8.0
|
||||
offset_bottom = -49.0
|
||||
|
||||
[node name="Label" type="Label" parent="CreateGroupDialog/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Enter name for new wobble sync group:"
|
||||
|
||||
[node name="LineEdit" type="LineEdit" parent="CreateGroupDialog/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
placeholder_text = "Group name..."
|
||||
@@ -2,9 +2,9 @@ extends NinePatchRect
|
||||
|
||||
@onready var spritePreview = $SpritePreview/Sprite2D
|
||||
@onready var outline = $Selected
|
||||
|
||||
|
||||
@onready var fade = $Fade
|
||||
@onready var button = $Button
|
||||
@onready var label = $Label
|
||||
|
||||
var sprite = null
|
||||
var parent = null
|
||||
@@ -14,9 +14,23 @@ var indent = 0
|
||||
var childrenTags = []
|
||||
var parentTag = null
|
||||
|
||||
## Context menu for right-click actions
|
||||
var context_menu: PopupMenu = null
|
||||
## Line edit for renaming
|
||||
var rename_edit: LineEdit = null
|
||||
## Track if we're currently in rename mode
|
||||
var is_renaming: bool = false
|
||||
|
||||
func _ready():
|
||||
var count = spritePath.get_slice_count("/") - 1
|
||||
$Label.text = spritePath.get_slice("/",count)
|
||||
# Use displayName if available, otherwise fallback to path
|
||||
var display_text = ""
|
||||
if sprite.displayName != "":
|
||||
display_text = sprite.displayName
|
||||
else:
|
||||
var count = spritePath.get_slice_count("/") - 1
|
||||
display_text = spritePath.get_slice("/",count)
|
||||
|
||||
label.text = display_text
|
||||
$Line2D.visible = false
|
||||
|
||||
spritePreview.texture = sprite.sprite.texture
|
||||
@@ -25,6 +39,115 @@ func _ready():
|
||||
spritePreview.scale = Vector2(1,1) * (60.0/displaySize)
|
||||
spritePreview.offset = sprite.sprite.offset
|
||||
|
||||
# Set up right-click detection
|
||||
button.gui_input.connect(_on_button_gui_input)
|
||||
|
||||
# Create context menu
|
||||
_setup_context_menu()
|
||||
|
||||
## Set up the right-click context menu
|
||||
func _setup_context_menu():
|
||||
context_menu = PopupMenu.new()
|
||||
add_child(context_menu)
|
||||
|
||||
# Add rename option
|
||||
context_menu.add_item("Rename", 0)
|
||||
context_menu.id_pressed.connect(_on_context_menu_selected)
|
||||
|
||||
# Wait for menu to be ready, then adjust size
|
||||
context_menu.ready.connect(_adjust_menu_size)
|
||||
|
||||
## Adjust the context menu size to be more compact
|
||||
func _adjust_menu_size():
|
||||
if context_menu:
|
||||
# Let the menu calculate its natural size first
|
||||
await get_tree().process_frame
|
||||
context_menu.reset_size() # Reset to content-based size
|
||||
|
||||
## Handle GUI input events on the button
|
||||
func _on_button_gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
||||
# Show context menu at cursor position
|
||||
var global_pos = get_global_mouse_position()
|
||||
context_menu.position = Vector2i(global_pos)
|
||||
context_menu.popup()
|
||||
|
||||
## Handle context menu selections
|
||||
func _on_context_menu_selected(id: int):
|
||||
match id:
|
||||
0: # Rename
|
||||
_start_rename()
|
||||
|
||||
## Start the rename process
|
||||
func _start_rename():
|
||||
if is_renaming:
|
||||
return
|
||||
|
||||
is_renaming = true
|
||||
|
||||
# Create line edit for renaming
|
||||
rename_edit = LineEdit.new()
|
||||
add_child(rename_edit)
|
||||
|
||||
# Position and size the line edit over the label
|
||||
rename_edit.position = label.position
|
||||
rename_edit.size = label.size
|
||||
rename_edit.text = label.text
|
||||
|
||||
# Hide the original label
|
||||
label.visible = false
|
||||
|
||||
# Focus the line edit and select all text
|
||||
rename_edit.grab_focus()
|
||||
rename_edit.select_all()
|
||||
|
||||
# Connect signals
|
||||
rename_edit.text_submitted.connect(_on_rename_submitted)
|
||||
rename_edit.focus_exited.connect(_on_rename_focus_lost)
|
||||
|
||||
## Handle rename submission (Enter key)
|
||||
func _on_rename_submitted(new_name: String):
|
||||
if rename_edit != null:
|
||||
_finish_rename(new_name)
|
||||
|
||||
## Handle losing focus (clicking elsewhere)
|
||||
func _on_rename_focus_lost():
|
||||
if rename_edit != null:
|
||||
_finish_rename(rename_edit.text)
|
||||
|
||||
## Finish the rename process
|
||||
func _finish_rename(new_name: String):
|
||||
if not is_renaming:
|
||||
return
|
||||
|
||||
is_renaming = false
|
||||
|
||||
# Clean up the line edit
|
||||
if rename_edit:
|
||||
rename_edit.queue_free()
|
||||
rename_edit = null
|
||||
|
||||
# Show the label again
|
||||
label.visible = true
|
||||
|
||||
# Validate and apply the new name
|
||||
new_name = new_name.strip_edges()
|
||||
if new_name != "" and new_name != label.text:
|
||||
_apply_rename(new_name)
|
||||
|
||||
## Apply the rename to the sprite
|
||||
func _apply_rename(new_name: String):
|
||||
# Update the display name (without file extension)
|
||||
label.text = new_name
|
||||
sprite.displayName = new_name # Store display name separately
|
||||
|
||||
# Update the global sprite list
|
||||
Global.spriteList.updateData()
|
||||
|
||||
# Show feedback to user
|
||||
Global.pushUpdate("Renamed sprite to: " + new_name)
|
||||
|
||||
|
||||
func updateChildren():
|
||||
for child in childrenTags:
|
||||
|
||||