Ship 0.8 fog/freistellen and fix Windows CI for friends
Some checks are pending
Windows Release / windows-x86_64 (push) Waiting to run
Some checks are pending
Windows Release / windows-x86_64 (push) Waiting to run
Publish the portable zip to public Forgejo from the private GitHub builder, and force Ninja so MSVC 195 deps build without CMake 4.2.
This commit is contained in:
parent
97b136de3b
commit
1c1d63f376
46 changed files with 1748 additions and 156 deletions
133
.github/workflows/windows_release.yml
vendored
133
.github/workflows/windows_release.yml
vendored
|
|
@ -1,7 +1,8 @@
|
|||
name: Windows Release
|
||||
|
||||
# Continuous Windows portable build on every push to main.
|
||||
# Uses GitHub-hosted windows-latest (MSVC preinstalled).
|
||||
# Uses GitHub-hosted windows-latest (MSVC) as a *private* builder.
|
||||
# Public download: Forgejo release tag windows-continuous.
|
||||
# See docs/windows-runner.md
|
||||
|
||||
on:
|
||||
|
|
@ -31,6 +32,9 @@ jobs:
|
|||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Setup Ninja
|
||||
uses: seanmiddleditch/gha-setup-ninja@v5
|
||||
|
||||
- name: Install Conan
|
||||
shell: pwsh
|
||||
run: |
|
||||
|
|
@ -44,6 +48,8 @@ jobs:
|
|||
$msvcVer = ($verLine.Line -split '=')[1].Trim()
|
||||
if (-not $msvcVer) { throw "could not read compiler.version from default profile" }
|
||||
# Host profile matching the runner (repo win-x86_64 still pins 194 for local Tower).
|
||||
# Force Ninja: recipes that tool_require cmake<4 cannot use "Visual Studio 18 2026"
|
||||
# (needs CMake >= 4.2). Ninja works with older CMake + MSVC 195.
|
||||
$p = Join-Path $env:USERPROFILE ".conan2\profiles\win-ci"
|
||||
@"
|
||||
[settings]
|
||||
|
|
@ -54,9 +60,13 @@ jobs:
|
|||
compiler.runtime=dynamic
|
||||
compiler.version=$msvcVer
|
||||
os=Windows
|
||||
|
||||
[conf]
|
||||
tools.cmake.cmaketoolchain:generator=Ninja
|
||||
"@.Trim() | Set-Content -Path $p -Encoding ascii
|
||||
Write-Host "win-ci MSVC version=$msvcVer"
|
||||
Write-Host "win-ci MSVC version=$msvcVer (generator=Ninja)"
|
||||
Get-Content $p
|
||||
ninja --version
|
||||
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
|
|
@ -81,8 +91,14 @@ jobs:
|
|||
if errorlevel 1 exit /b 1
|
||||
conan install . -of=build-x86_64 --build=missing -pr:h=win-ci -pr:b=default
|
||||
if errorlevel 1 exit /b 1
|
||||
call windowsinstall\build_x86_64.bat
|
||||
if not exist build-x86_64\build mkdir build-x86_64\build
|
||||
cd build-x86_64\build
|
||||
call .\generators\conanbuild.bat
|
||||
cmake ..\.. -G Ninja -DCMAKE_TOOLCHAIN_FILE="generators\conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Release -DCONFIG_NEXT_TO_EXECUTABLE=OFF
|
||||
if errorlevel 1 exit /b 1
|
||||
cmake --build . --config Release
|
||||
if errorlevel 1 exit /b 1
|
||||
call .\generators\deactivate_conanbuild.bat
|
||||
|
||||
- name: Package portable zip
|
||||
id: pkg
|
||||
|
|
@ -92,9 +108,9 @@ jobs:
|
|||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$exe = Get-ChildItem -Path . -Recurse -Filter infinipaint.exe |
|
||||
Where-Object { $_.FullName -match 'Release' } |
|
||||
Where-Object { $_.FullName -match 'build-x86_64|Release' } |
|
||||
Select-Object -First 1
|
||||
if (-not $exe) { throw "infinipaint.exe not found under Release" }
|
||||
if (-not $exe) { throw "infinipaint.exe not found under build-x86_64/Release" }
|
||||
$outDir = Join-Path (Get-Location) "dist\portable"
|
||||
Remove-Item -Recurse -Force $outDir -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||
|
|
@ -106,6 +122,14 @@ jobs:
|
|||
$dataSrc = Join-Path (Get-Location) "assets\data"
|
||||
if (-not (Test-Path $dataSrc)) { $dataSrc = Join-Path (Get-Location) "data" }
|
||||
Copy-Item -Recurse $dataSrc (Join-Path $outDir "data")
|
||||
$rembgSrc = Join-Path (Get-Location) "third_party\rembg"
|
||||
if (Test-Path $rembgSrc) {
|
||||
Copy-Item -Recurse $rembgSrc (Join-Path $outDir "rembg")
|
||||
}
|
||||
$modelsSrc = Join-Path (Get-Location) "third_party\rembg-home"
|
||||
if (Test-Path $modelsSrc) {
|
||||
Copy-Item -Recurse $modelsSrc (Join-Path $outDir "rembg-home")
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path "dist" | Out-Null
|
||||
$zipPath = Join-Path (Get-Location) "dist\$env:ZIP_NAME"
|
||||
if (Test-Path $zipPath) { Remove-Item $zipPath }
|
||||
|
|
@ -113,19 +137,110 @@ jobs:
|
|||
"zip_path=$zipPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
Get-Item $zipPath | Format-List FullName, Length
|
||||
|
||||
- name: Publish GitHub release
|
||||
# Optional private artifact on GitHub (repo is private builder-only).
|
||||
- name: Publish private GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.ver.outputs.tag }}
|
||||
name: Windows continuous (${{ steps.ver.outputs.asset_ver }})
|
||||
prerelease: true
|
||||
body: |
|
||||
Continuous Windows build from `main`.
|
||||
Continuous Windows build from `main` (private GitHub builder).
|
||||
|
||||
**Public download:** Forgejo release `windows-continuous`
|
||||
https://forgejo.fsociety00.cc/copilot/infinipaint/releases/tag/windows-continuous
|
||||
|
||||
- Version: **${{ steps.ver.outputs.version }}**
|
||||
- Commit: `${{ github.sha }}`
|
||||
- Asset: `${{ steps.ver.outputs.zip_name }}`
|
||||
|
||||
Updated on every push to `main`. Built on GitHub-hosted `windows-latest`.
|
||||
files: dist/${{ steps.ver.outputs.zip_name }}
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
- name: Publish Forgejo release (public)
|
||||
shell: bash
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
VERSION_TAG: ${{ steps.ver.outputs.tag }}
|
||||
ASSET_VER: ${{ steps.ver.outputs.asset_ver }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
ZIP_NAME: ${{ steps.ver.outputs.zip_name }}
|
||||
FORGEJO_API: https://forgejo.fsociety00.cc/api/v1
|
||||
FORGEJO_REPO: copilot/infinipaint
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${FORGEJO_TOKEN:-}" ]; then
|
||||
echo "FORGEJO_TOKEN secret missing — cannot publish public Forgejo release" >&2
|
||||
exit 1
|
||||
fi
|
||||
zip_path="dist/${ZIP_NAME}"
|
||||
test -f "$zip_path"
|
||||
python3 - <<'PY'
|
||||
import json, os, ssl, urllib.error, urllib.parse, urllib.request
|
||||
|
||||
api = os.environ["FORGEJO_API"].rstrip("/")
|
||||
repo = os.environ["FORGEJO_REPO"]
|
||||
token = os.environ["FORGEJO_TOKEN"]
|
||||
tag = os.environ["VERSION_TAG"]
|
||||
name = f"Windows continuous ({os.environ['ASSET_VER']})"
|
||||
zip_name = os.environ["ZIP_NAME"]
|
||||
zip_path = f"dist/{zip_name}"
|
||||
body = "\n".join([
|
||||
"Continuous Windows build from `main` (built on private GitHub Actions).",
|
||||
"",
|
||||
f"- Version: **{os.environ['VERSION']}**",
|
||||
f"- Commit: `{os.environ['GITHUB_SHA']}`",
|
||||
f"- Asset: `{zip_name}`",
|
||||
"",
|
||||
"Updated on every push to `main`.",
|
||||
])
|
||||
ctx = ssl.create_default_context()
|
||||
if os.environ.get("FORGEJO_CURL_INSECURE", "").lower() in ("1", "true"):
|
||||
ctx = ssl._create_unverified_context()
|
||||
|
||||
def req(method, url, data=None, headers=None, raw=False, allow_404=False):
|
||||
h = {"Authorization": f"token {token}"}
|
||||
if headers:
|
||||
h.update(headers)
|
||||
body_data = data
|
||||
if data is not None and not raw:
|
||||
body_data = json.dumps(data).encode()
|
||||
h.setdefault("Content-Type", "application/json")
|
||||
r = urllib.request.Request(url, data=body_data, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(r, context=ctx) as resp:
|
||||
raw_body = resp.read()
|
||||
return resp.status, (json.loads(raw_body) if raw_body else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
err = e.read().decode(errors="replace")
|
||||
if allow_404 and e.code == 404:
|
||||
return 404, {}
|
||||
raise SystemExit(f"{method} {url} -> {e.code}: {err}") from e
|
||||
|
||||
base = f"{api}/repos/{repo}/releases"
|
||||
status, release = req("GET", f"{base}/tags/{urllib.parse.quote(tag)}", allow_404=True)
|
||||
payload = {
|
||||
"tag_name": tag,
|
||||
"name": name,
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": True,
|
||||
}
|
||||
if status == 404:
|
||||
_, release = req("POST", base, payload)
|
||||
else:
|
||||
release_id = release["id"]
|
||||
_, release = req("PATCH", f"{base}/{release_id}", payload)
|
||||
for asset in release.get("assets") or []:
|
||||
if asset.get("name") == zip_name:
|
||||
req("DELETE", f"{base}/{release_id}/assets/{asset['id']}")
|
||||
|
||||
release_id = release["id"]
|
||||
with open(zip_path, "rb") as f:
|
||||
data = f.read()
|
||||
upload_url = f"{base}/{release_id}/assets?name={urllib.parse.quote(zip_name)}"
|
||||
_, asset = req(
|
||||
"POST", upload_url, data=data, raw=True,
|
||||
headers={"Content-Type": "application/octet-stream"},
|
||||
)
|
||||
print(asset.get("browser_download_url") or asset.get("name") or asset)
|
||||
PY
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ set(sources "src/main.cpp"
|
|||
"src/DrawingProgram/Layers/DrawingProgramLayerListItem.cpp"
|
||||
"src/DrawingProgram/Layers/DrawingProgramLayerFolder.cpp"
|
||||
"src/DrawingProgram/Layers/DrawingProgramLayer.cpp"
|
||||
"src/DrawingProgram/Layers/FogStripper.cpp"
|
||||
"src/DrawingProgram/Layers/SerializedBlendMode.cpp"
|
||||
"src/CanvasComponents/CanvasComponentContainer.cpp"
|
||||
"src/CanvasComponents/CanvasComponentAllocator.cpp"
|
||||
|
|
@ -222,7 +223,7 @@ else()
|
|||
find_package(clipper2 REQUIRED)
|
||||
endif()
|
||||
|
||||
find_package(ICU 77.1 REQUIRED COMPONENTS uc)
|
||||
find_package(ICU REQUIRED COMPONENTS uc)
|
||||
|
||||
if(NOT EMSCRIPTEN)
|
||||
if((NOT ANDROID) AND (NOT LINUX))
|
||||
|
|
@ -423,8 +424,12 @@ else()
|
|||
else()
|
||||
target_link_libraries(main sdl-infinipaint::sdl-infinipaint)
|
||||
endif()
|
||||
if(LINUX)
|
||||
target_link_libraries(main ICU::uc)
|
||||
else()
|
||||
target_link_libraries(main icu-infinipaint::icu-infinipaint)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT EMSCRIPTEN)
|
||||
target_link_libraries(main harfbuzz::harfbuzz)
|
||||
|
|
|
|||
21
FORK.md
21
FORK.md
|
|
@ -1,21 +1,20 @@
|
|||
# Fork features (copilot/infinipaint)
|
||||
|
||||
Upstream: [ErrorAtLine0/infinipaint](https://github.com/ErrorAtLine0/infinipaint)
|
||||
Forgejo: https://forgejo.fsociety00.cc/copilot/infinipaint (public)
|
||||
GitHub (CI / Windows zip): https://github.com/littlecodedragon/infinipaint
|
||||
**Public forge:** https://forgejo.fsociety00.cc/copilot/infinipaint
|
||||
GitHub (`littlecodedragon/infinipaint`) is **private builder only** — not a public fork mirror. Source of truth stays on Forgejo.
|
||||
|
||||
Save format: `INFPNT000007` / version `0.7.0-fork` (not network-compatible with stock 0.6.x peers).
|
||||
Save format: `INFPNT000008` / version `0.8.0-fork` (not network-compatible with stock 0.6.x peers).
|
||||
|
||||
## CI
|
||||
|
||||
| Workflow | Trigger | Runner | Output |
|
||||
|----------|---------|--------|--------|
|
||||
| `.github/workflows/windows_release.yml` | every push to `main` | GitHub-hosted **`windows-latest`** | prerelease tag `windows-continuous` |
|
||||
| `.github/workflows/windows_release.yml` | push `main` to private GitHub mirror | GitHub-hosted **`windows-latest`** | uploads public Forgejo tag `windows-continuous` |
|
||||
| `.forgejo/workflows/windows_release.yml` | every push to `main` | Forgejo self-hosted `windows-release` (optional) | same tag on Forgejo |
|
||||
|
||||
Primary path: GitHub Actions — see [docs/windows-runner.md](docs/windows-runner.md).
|
||||
Download: https://github.com/littlecodedragon/infinipaint/releases/tag/windows-continuous
|
||||
|
||||
**Friends (Windows):** https://forgejo.fsociety00.cc/copilot/infinipaint/releases/tag/windows-continuous — zip entpacken, `infinipaint.exe` starten.
|
||||
Details: [docs/windows-runner.md](docs/windows-runner.md).
|
||||
## Bring Everyone Here (host)
|
||||
|
||||
In the player list, the lobby host gets **Bring Everyone Here**. Clients receive `CLIENT_FORCE_CAMERA_JUMP` and run the same `smooth_move_to` path as **Jump To**.
|
||||
|
|
@ -26,8 +25,12 @@ With a multi-selection (images, brush strokes, text boxes, shapes, …), use **G
|
|||
|
||||
## Image compression
|
||||
|
||||
Settings → **Images**: compress on insert (WebP/PNG/Keep). Selection: **Compress Images**.
|
||||
Settings → **Images**: compress on insert (WebP/PNG/Keep). **Compress all images on canvas** rewrites every already-placed raster (same format/quality; Keep uses WebP). Selection panel: **Compress Images** for a subset.
|
||||
|
||||
## Freistellen
|
||||
|
||||
Settings → **Images**: white-key and optional rembg (Tower `silueta` / `bria-rmbg`). Selection: **Freistellen**.
|
||||
Not automatic on import. Choose **Paste Image + Freistellen** from the canvas right-click menu, or **Ctrl+Shift+V** then pick Freistellen vs as-is. Right-click an already placed image (or use the selection panel / image properties) to Freistellen later. Settings → **Images** only pick the method: white-key always; **Schnell (silueta)** / **Allgemein (bria)** when rembg CLI and that model exist (bundled `rembg/` + `rembg-home/models/`, Tower `~/.local/share/tower-rembg`, or PATH). Otherwise those radios stay greyed; white-key still works. Linux install runs `linuxinstall/bundle-rembg.sh`.
|
||||
|
||||
## Fog of War / layer lock (host)
|
||||
|
||||
In the layer panel (host / local file only): mark a leaf layer as **Fog of War layer** (must sit **above** the content it covers), then paint fog with brush/shapes on that layer. Players receive a **stripped** world — only objects **fully** covered by opaque fog (not merely overlapping) are omitted from join sync and from their save. Partially covered images stay. Moving or reshaping fog (or content under it) live-updates clients: newly covered objects are unloaded, revealed ones are sent again with their resources. The host file stays complete. Fog draws at the layer’s real alpha (1.0 = fully opaque for host and players). **Lock for players** blocks client edits on a normal layer; fog layers are always locked for players (move/edit/erase rejected on the host).
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
InfiniPaint is a **collaborative, infinite** canvas note-taking/drawing app. Unlike many other infinite canvas apps, **there is no zoom-in or zoom-out limit** (at least up until the point your computer runs out of memory). This means that this app is very good at things such as drawing sketches of the solar system to scale, or just drawing any massive objects with tiny details. Of course, even though this is a feature, this app is also perfectly well suited for use as a normal canvas.
|
||||
|
||||
> **This repository is a fork** — Forgejo: [copilot/infinipaint](https://forgejo.fsociety00.cc/copilot/infinipaint), GitHub (Windows CI): [littlecodedragon/infinipaint](https://github.com/littlecodedragon/infinipaint). Extra features are documented in [FORK.md](FORK.md).
|
||||
> **This repository is a fork** — public source: [copilot/infinipaint](https://forgejo.fsociety00.cc/copilot/infinipaint) on Forgejo. Extra features: [FORK.md](FORK.md). (A private GitHub mirror may exist only for Windows CI — not a public download site.)
|
||||
|
||||
You can try the web version of this app at [infinipaint.com](https://infinipaint.com) (requires a WebGL2 capable browser). The web version was compiled from C++ to Javascript using Emscripten, and may contain some bugs, so if possible, please consider downloading the native version for a better experience.
|
||||
|
||||
|
|
|
|||
|
|
@ -509,8 +509,58 @@ class ConanSkia(ConanFile):
|
|||
return [fw + ".framework" for fw in frameworks]
|
||||
|
||||
def build(self):
|
||||
# Optional: reuse a finished out/conan from a prior local build
|
||||
# (avoids re-hitting googlesource + full ninja). Path = .../out/conan
|
||||
reuse_out = os.environ.get("SKIA_REUSE_OUT")
|
||||
if reuse_out and os.path.isdir(reuse_out):
|
||||
import shutil
|
||||
dst = join(self.source_folder, "out", "conan")
|
||||
os.makedirs(join(self.source_folder, "out"), exist_ok=True)
|
||||
if os.path.isdir(dst):
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(reuse_out, dst)
|
||||
return
|
||||
|
||||
# activate-emsdk fails on Windows for some reason.
|
||||
os.environ["GIT_SYNC_DEPS_SKIP_EMSDK"] = "1"
|
||||
# Default git-sync-deps fans out one thread per dep and hits
|
||||
# googlesource HTTP 429. Serialize + retry before syncing.
|
||||
replace_in_file(
|
||||
self,
|
||||
join(self.source_folder, "tools", "git-sync-deps"),
|
||||
"""def multithread(function, list_of_arg_lists):
|
||||
anything_failed = False
|
||||
threads = []
|
||||
def hook(args):
|
||||
nonlocal anything_failed
|
||||
anything_failed = True
|
||||
threading.excepthook = hook
|
||||
for args in list_of_arg_lists:
|
||||
thread = threading.Thread(None, function, None, args)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
if anything_failed:
|
||||
raise Exception("Thread failure detected")
|
||||
""",
|
||||
"""def multithread(function, list_of_arg_lists):
|
||||
import time
|
||||
for args in list_of_arg_lists:
|
||||
last_err = None
|
||||
for attempt in range(6):
|
||||
try:
|
||||
function(*args)
|
||||
last_err = None
|
||||
break
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
time.sleep(min(60, 2 ** attempt))
|
||||
if last_err is not None:
|
||||
raise last_err
|
||||
""",
|
||||
strict=True,
|
||||
)
|
||||
self.run("python3 tools/git-sync-deps")
|
||||
del os.environ["GIT_SYNC_DEPS_SKIP_EMSDK"]
|
||||
|
||||
|
|
@ -585,9 +635,21 @@ class ConanSkia(ConanFile):
|
|||
|
||||
buildenv = VirtualBuildEnv(self).vars()
|
||||
|
||||
if cc := buildenv.get("CC", default=None):
|
||||
cc = buildenv.get("CC", default=None)
|
||||
cxx = buildenv.get("CXX", default=None)
|
||||
# Fall back to tools.build:compiler_executables when VirtualBuildEnv
|
||||
# did not inject CC/CXX (common with only profile [conf]).
|
||||
try:
|
||||
executables = self.conf.get("tools.build:compiler_executables", default={}) or {}
|
||||
except Exception:
|
||||
executables = {}
|
||||
if not cc:
|
||||
cc = executables.get("c")
|
||||
if not cxx:
|
||||
cxx = executables.get("cpp")
|
||||
if cc:
|
||||
args += f"cc=\"{cc}\"\n"
|
||||
if cxx := buildenv.get("CXX", default=None):
|
||||
if cxx:
|
||||
args += f"cxx=\"{cxx}\"\n"
|
||||
if ar := buildenv.get("AR", default=None):
|
||||
args += f"ar=\"{ar}\"\n"
|
||||
|
|
|
|||
|
|
@ -216,8 +216,34 @@ class ICUConan(ConanFile):
|
|||
if dat_package_file:
|
||||
shutil.copy(str(self.options.dat_package_file), dat_package_file[0])
|
||||
|
||||
# GCC 16 ICEs compiling ICU 77 headers under -std=gnu++23 (enum in uchar.h).
|
||||
# Keep the package usable from a C++23 consumer, but build ICU itself as C++17.
|
||||
if self.settings.os == "Linux":
|
||||
import os as _os
|
||||
for var in ("CXXFLAGS", "CFLAGS"):
|
||||
cur = _os.environ.get(var, "")
|
||||
parts = [p for p in cur.split() if not p.startswith("-std=") and not p.startswith("-O")]
|
||||
if var == "CXXFLAGS":
|
||||
parts.extend(["-std=gnu++17", "-O1"])
|
||||
else:
|
||||
parts.append("-O1")
|
||||
_os.environ[var] = " ".join(parts)
|
||||
|
||||
autotools = Autotools(self)
|
||||
autotools.configure(build_script_folder=os.path.join(self.source_folder, "source"))
|
||||
|
||||
if self.settings.os == "Linux":
|
||||
for root, _, files in os.walk(self.build_folder):
|
||||
for name in files:
|
||||
if name in ("Makefile", "mh-linux") or name.endswith(".mk"):
|
||||
path = os.path.join(root, name)
|
||||
try:
|
||||
replace_in_file(self, path, "-O3", "-O1", strict=False)
|
||||
replace_in_file(self, path, "-std=gnu++23", "-std=gnu++17", strict=False)
|
||||
replace_in_file(self, path, "-std=c++23", "-std=c++17", strict=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
autotools.make()
|
||||
|
||||
@property
|
||||
|
|
|
|||
14
conan/profiles/linux-x86_64-clang
Normal file
14
conan/profiles/linux-x86_64-clang
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[settings]
|
||||
arch=x86_64
|
||||
build_type=Release
|
||||
compiler=clang
|
||||
compiler.cppstd=gnu23
|
||||
compiler.libcxx=libstdc++11
|
||||
compiler.version=22
|
||||
os=Linux
|
||||
|
||||
[tool_requires]
|
||||
cmake/3.27.0
|
||||
|
||||
[conf]
|
||||
tools.build:compiler_executables={'c':'clang','cpp':'clang++'}
|
||||
21
conan/profiles/linux-x86_64-gcc15
Normal file
21
conan/profiles/linux-x86_64-gcc15
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[settings]
|
||||
arch=x86_64
|
||||
build_type=Release
|
||||
compiler=gcc
|
||||
compiler.cppstd=gnu23
|
||||
compiler.libcxx=libstdc++11
|
||||
compiler.version=15
|
||||
os=Linux
|
||||
|
||||
[tool_requires]
|
||||
cmake/3.27.0
|
||||
|
||||
[buildenv]
|
||||
CC=/home/yuna/.local/share/gcc15-root/usr/bin/gcc-15
|
||||
CXX=/home/yuna/.local/share/gcc15-root/usr/bin/g++-15
|
||||
|
||||
[conf]
|
||||
tools.build:compiler_executables={'c':'/home/yuna/.local/share/gcc15-root/usr/bin/gcc-15','cpp':'/home/yuna/.local/share/gcc15-root/usr/bin/g++-15'}
|
||||
tools.build:jobs=2
|
||||
tools.build:cflags=["-O2"]
|
||||
tools.build:cxxflags=["-O2"]
|
||||
|
|
@ -95,7 +95,8 @@ class CompressorRecipe(ConanFile):
|
|||
"use_system_harfbuzz": True,
|
||||
"use_conan_harfbuzz": True,
|
||||
"use_system_icu": True,
|
||||
"use_conan_icu": True,
|
||||
# Real distro ICU — Conan ICU 77 ICEs on GCC 16 / Clang 22.
|
||||
"use_conan_icu": False,
|
||||
"use_system_libjpeg_turbo": False,
|
||||
"use_system_libpng": False,
|
||||
"use_system_libwebp": False,
|
||||
|
|
@ -129,6 +130,8 @@ class CompressorRecipe(ConanFile):
|
|||
|
||||
self.requires("clipper2/2.0.1")
|
||||
self.requires("zstd/1.5.7")
|
||||
# Skip Conan ICU on Linux (use system ICU via CMake find_package).
|
||||
if self.settings.os != "Linux":
|
||||
self.requires("icu-infinipaint/77.1")
|
||||
|
||||
def build_requirements(self):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
# Windows continuous release (GitHub Actions)
|
||||
# Windows continuous release (GitHub Actions as private builder)
|
||||
|
||||
Native InfiniPaint Windows builds need **MSVC** (see `docs/BUILDING.md` +
|
||||
`windowsinstall/`). There is no Linux cross-build path.
|
||||
|
||||
The fork publishes a portable zip from a **GitHub** repository using the
|
||||
GitHub-hosted runner `windows-latest` (Visual Studio / MSVC preinstalled).
|
||||
Policy: **Forgejo is the public fork**; GitHub is only a **private** mirror used
|
||||
to run `windows-latest` CI. Friends download the zip from Forgejo — not GitHub.
|
||||
|
||||
## Public download (friends)
|
||||
|
||||
https://forgejo.fsociety00.cc/copilot/infinipaint/releases/tag/windows-continuous
|
||||
|
||||
Portable zip → extract → run `infinipaint.exe`.
|
||||
|
||||
## Workflow
|
||||
|
||||
|
|
@ -12,30 +18,30 @@ File: `.github/workflows/windows_release.yml`
|
|||
|
||||
| | |
|
||||
|---|---|
|
||||
| Trigger | every push to `main`, plus `workflow_dispatch` |
|
||||
| Runner | `windows-latest` (GitHub-hosted, MSVC ~195) |
|
||||
| Build | Conan profile `win-ci` (detected) + `windowsinstall/build_x86_64.bat` |
|
||||
| Output | prerelease tag **`windows-continuous`** with a portable zip |
|
||||
| Trigger | every push to `main` on the private GitHub mirror, plus `workflow_dispatch` |
|
||||
| Runner | `windows-latest` (GitHub-hosted, MSVC ~195 / VS 18) |
|
||||
| Build | Conan profile `win-ci` + **Ninja** (avoids CMake<4 vs `Visual Studio 18 2026`) |
|
||||
| Output | public Forgejo prerelease **`windows-continuous`** (+ private GitHub copy) |
|
||||
|
||||
Required GitHub Actions secret: `FORGEJO_TOKEN` (Forgejo PAT with release write on
|
||||
`copilot/infinipaint`).
|
||||
|
||||
Local Tower Windows can keep using `windowsinstall/conan_init_x86_64.bat`
|
||||
(`conan/profiles/win-x86_64`, MSVC 194). CI generates a `win-ci` profile for
|
||||
whatever MSVC `windows-latest` ships.
|
||||
|
||||
## GitHub remote
|
||||
|
||||
Recommended remote name: `github`
|
||||
## GitHub remote (private builder)
|
||||
|
||||
```bash
|
||||
git remote add github https://github.com/littlecodedragon/infinipaint.git
|
||||
git push -u github main
|
||||
```
|
||||
|
||||
Releases appear under:
|
||||
https://github.com/littlecodedragon/infinipaint/releases/tag/windows-continuous
|
||||
Keep the repo **private** (Settings → Change visibility, or
|
||||
`gh repo edit littlecodedragon/infinipaint --visibility private`).
|
||||
|
||||
## Forgejo note
|
||||
|
||||
`.forgejo/workflows/windows_release.yml` still targets a Forgejo self-hosted
|
||||
label `windows-release` (Tower dual-boot). Prefer the GitHub workflow above for
|
||||
unattended continuous Windows zips; keep Forgejo for the public forge mirror if
|
||||
desired.
|
||||
`.forgejo/workflows/windows_release.yml` still targets a self-hosted
|
||||
`windows-release` runner (optional). Primary public zips come from the GitHub
|
||||
builder → Forgejo release upload above.
|
||||
|
|
|
|||
27
linuxinstall/bundle-rembg.sh
Executable file
27
linuxinstall/bundle-rembg.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
# Place rembg CLI + models next to an InfiniPaint install so Schnell/Allgemein
|
||||
# work without a global PATH dependency. Missing tools are skipped; the UI greys out.
|
||||
set -euo pipefail
|
||||
DEST="${1:?usage: bundle-rembg.sh DEST_DIR}"
|
||||
mkdir -p "$DEST"
|
||||
|
||||
TOWER="${TOWER_REMBG_VENV:-$HOME/.local/share/tower-rembg}"
|
||||
MODELS_SRC="${REMBG_HOME:-$HOME/.rembg}"
|
||||
|
||||
if [[ -x "$TOWER/bin/rembg" ]]; then
|
||||
ln -sfn "$TOWER" "$DEST/rembg"
|
||||
echo "Linked rembg CLI: $DEST/rembg -> $TOWER"
|
||||
elif [[ -x "$TOWER/Scripts/rembg.exe" ]]; then
|
||||
ln -sfn "$TOWER" "$DEST/rembg"
|
||||
echo "Linked rembg CLI: $DEST/rembg -> $TOWER"
|
||||
else
|
||||
echo "No tower-rembg venv at $TOWER — Schnell/Allgemein will be unavailable unless rembg is on PATH."
|
||||
fi
|
||||
|
||||
if [[ -d "$MODELS_SRC/models" ]]; then
|
||||
mkdir -p "$DEST/rembg-home"
|
||||
ln -sfn "$MODELS_SRC/models" "$DEST/rembg-home/models"
|
||||
echo "Linked rembg models: $DEST/rembg-home/models -> $MODELS_SRC/models"
|
||||
else
|
||||
echo "No rembg models at $MODELS_SRC/models — download with: rembg d silueta && rembg d bria-rmbg"
|
||||
fi
|
||||
|
|
@ -14,6 +14,11 @@ rm -rf "${APPDIR}/data"
|
|||
cp -a "${ROOT}/assets/data" "${APPDIR}/data"
|
||||
ln -sfn "${APPDIR}/infinipaint" "${PREFIX}/bin/infinipaint"
|
||||
|
||||
# Optional rembg next to the app (silueta / bria). Missing = greyed-out modes in Settings.
|
||||
if [[ -x "${ROOT}/linuxinstall/bundle-rembg.sh" ]]; then
|
||||
bash "${ROOT}/linuxinstall/bundle-rembg.sh" "$APPDIR" || true
|
||||
fi
|
||||
|
||||
cat > "${PREFIX}/share/applications/com.infinipaint.infinipaint-fork.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=InfiniPaint (fork)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include "CanvasComponentAllocator.hpp"
|
||||
#include "CanvasComponentContainer.hpp"
|
||||
#include "../DrawingProgram/DrawingProgram.hpp"
|
||||
#include "../DrawingProgram/Layers/DrawingProgramLayerListItem.hpp"
|
||||
#include "../World.hpp"
|
||||
#include <Helpers/NetworkingObjects/DelayUpdateSerializedClassManager.hpp>
|
||||
#include "CanvasComponent.hpp"
|
||||
|
|
@ -45,6 +46,13 @@ void CanvasComponentAllocator::register_class(World& world) {
|
|||
a(*o.comp);
|
||||
},
|
||||
.readUpdate = [](CanvasComponentAllocator& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr<NetServer::ClientData>& c) {
|
||||
// Host rejects client edits of fog/locked-layer components (still consume the payload).
|
||||
if(c && o.comp && o.comp->compContainer && o.comp->compContainer->parentLayer
|
||||
&& o.comp->compContainer->parentLayer->is_player_edit_blocked()) {
|
||||
auto junk = std::unique_ptr<CanvasComponent>(CanvasComponent::allocate_comp(o.comp->get_type()));
|
||||
a(*junk);
|
||||
return;
|
||||
}
|
||||
a(*o.comp);
|
||||
},
|
||||
.allocateCopy = [](const CanvasComponentAllocator& o) {
|
||||
|
|
@ -58,6 +66,8 @@ void CanvasComponentAllocator::register_class(World& world) {
|
|||
},
|
||||
.postUpdateFunc = [&drawP = world.drawProg](CanvasComponentAllocator& o) {
|
||||
o.comp->compContainer->commit_update(drawP);
|
||||
// Fog mesh reshape / content edit can change who is fully covered.
|
||||
drawP.layerMan.mark_fog_client_projection_dirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ namespace CustomEvents {
|
|||
std::string data;
|
||||
std::optional<RichText::TextData> richText;
|
||||
std::optional<Vector2f> mousePos;
|
||||
bool applyFreistellen = false;
|
||||
};
|
||||
|
||||
struct OpenInfiniPaintFileEvent {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
#include "../ImagePreprocess.hpp"
|
||||
#include <Helpers/Random.hpp>
|
||||
#include <cereal/types/utility.hpp>
|
||||
#include <unordered_map>
|
||||
#ifdef __ANDROID__
|
||||
#include "../AndroidJNICalls.hpp"
|
||||
#endif
|
||||
|
|
@ -112,6 +113,8 @@ void DrawingProgram::input_drop_text_callback(const InputManager::DropCallbackAr
|
|||
img.d.p2 = drop.pos + imDim;
|
||||
img.d.imageID = {0, 0};
|
||||
auto newObjInfo = layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!newObjInfo)
|
||||
return;
|
||||
layerMan.add_undo_place_component(newObjInfo);
|
||||
droppedDownloadingFiles.emplace_back(newObjInfo, world.main.window.size.cast<float>(), FileDownloader::download_data_from_url(drop.data));
|
||||
}
|
||||
|
|
@ -339,8 +342,28 @@ void DrawingProgram::init_server_callbacks() {
|
|||
coord.scale_about(WorldVec{0, 0}, scaleUpAmount, true);
|
||||
}
|
||||
|
||||
process_transform_message(transforms);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, transforms);
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>> allowed;
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>> rejectedRestore;
|
||||
allowed.reserve(transforms.size());
|
||||
for(auto& [netID, coord] : transforms) {
|
||||
auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer>(netID);
|
||||
if(!objPtr)
|
||||
continue;
|
||||
if(!layerMan.client_may_modify_component(objPtr.get())) {
|
||||
rejectedRestore.emplace_back(netID, objPtr->coords);
|
||||
continue;
|
||||
}
|
||||
allowed.emplace_back(netID, coord);
|
||||
}
|
||||
if(!allowed.empty()) {
|
||||
process_transform_message(allowed);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, allowed);
|
||||
// Moving content or fog changes coverage — unload/reload on clients.
|
||||
layerMan.refresh_fog_client_projection();
|
||||
}
|
||||
// Snap the offending client (and peers) back if they tried to move fog/locked objects.
|
||||
if(!rejectedRestore.empty())
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, rejectedRestore);
|
||||
});
|
||||
world.netServer->add_recv_callback(SERVER_FORCE_CAMERA_JUMP, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
CoordSpaceHelper coords;
|
||||
|
|
@ -355,8 +378,18 @@ void DrawingProgram::init_server_callbacks() {
|
|||
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> groupIds;
|
||||
message(groupIds);
|
||||
(void)client;
|
||||
process_group_id_message(groupIds);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_SET_COMPONENT_GROUP_IDS, groupIds);
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> allowed;
|
||||
allowed.reserve(groupIds.size());
|
||||
for(auto& [netID, groupId] : groupIds) {
|
||||
auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer>(netID);
|
||||
if(!objPtr || !layerMan.client_may_modify_component(objPtr.get()))
|
||||
continue;
|
||||
allowed.emplace_back(netID, groupId);
|
||||
}
|
||||
if(allowed.empty())
|
||||
return;
|
||||
process_group_id_message(allowed);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_SET_COMPONENT_GROUP_IDS, allowed);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -430,10 +463,20 @@ void DrawingProgram::process_transform_message(const std::vector<std::pair<Netwo
|
|||
void DrawingProgram::send_transforms_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objsToSendTransformsFor) {
|
||||
if(world.netObjMan.is_connected()) {
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>> transforms;
|
||||
for(auto& obj : objsToSendTransformsFor)
|
||||
for(auto& obj : objsToSendTransformsFor) {
|
||||
if(!obj || !obj->obj)
|
||||
continue;
|
||||
if(!world.netObjMan.is_server() && !layerMan.client_may_modify_component(obj->obj.get()))
|
||||
continue;
|
||||
transforms.emplace_back(obj->obj.get_net_id(), obj->obj->coords);
|
||||
if(world.netObjMan.is_server())
|
||||
}
|
||||
if(transforms.empty())
|
||||
return;
|
||||
if(world.netObjMan.is_server()) {
|
||||
// Fog coords first, then coverage erase/reveal (same reliable channel order).
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, transforms);
|
||||
layerMan.refresh_fog_client_projection();
|
||||
}
|
||||
else
|
||||
world.netClient->send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_TRANSFORM_MANY_COMPONENTS, transforms);
|
||||
}
|
||||
|
|
@ -497,9 +540,13 @@ void DrawingProgram::toolbar_gui(Toolbar& t) {
|
|||
}
|
||||
|
||||
void DrawingProgram::right_click_popup_gui(Toolbar& t) {
|
||||
if(rightClickPopupLocation.has_value())
|
||||
if(rightClickPopupLocation.has_value()) {
|
||||
if(showingImagePasteChooser)
|
||||
image_paste_choice_menu(rightClickPopupLocation.value());
|
||||
else
|
||||
drawTool->right_click_popup_gui(t, rightClickPopupLocation.value());
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgram::set_right_click_popup_location(const Vector2f& newLoc) {
|
||||
if(!rightClickPopupLocation.has_value() || rightClickPopupLocation.value() != newLoc) {
|
||||
|
|
@ -509,8 +556,9 @@ void DrawingProgram::set_right_click_popup_location(const Vector2f& newLoc) {
|
|||
}
|
||||
|
||||
void DrawingProgram::clear_right_click_popup() {
|
||||
if(rightClickPopupLocation.has_value()) {
|
||||
if(rightClickPopupLocation.has_value() || showingImagePasteChooser) {
|
||||
rightClickPopupLocation = std::nullopt;
|
||||
showingImagePasteChooser = false;
|
||||
world.main.g.gui.set_to_layout();
|
||||
}
|
||||
}
|
||||
|
|
@ -550,11 +598,43 @@ void DrawingProgram::selection_action_menu(Vector2f popupPos) {
|
|||
});
|
||||
popup_menu_action_button("Paste Image", "Paste Image", [&, popupPos] {
|
||||
selection.deselect_all();
|
||||
world.main.input.call_paste(CustomEvents::PasteEvent::DataType::IMAGE, {
|
||||
.pastePosition = popupPos * world.main.g.final_gui_scale()
|
||||
request_clipboard_image_paste(popupPos * world.main.g.final_gui_scale(), false);
|
||||
});
|
||||
popup_menu_action_button("Paste Image Freistellen", "Paste Image + Freistellen", [&, popupPos] {
|
||||
selection.deselect_all();
|
||||
request_clipboard_image_paste(popupPos * world.main.g.final_gui_scale(), true);
|
||||
});
|
||||
CanvasComponentContainer::ObjInfo* imageUnderCursor = nullptr;
|
||||
{
|
||||
Vector2f screenPos = popupPos * world.main.g.final_gui_scale();
|
||||
SkPath clickPath = SkPath::Rect(SkRect::MakeLTRB(screenPos.x() - 0.5f, screenPos.y() - 0.5f, screenPos.x() + 0.5f, screenPos.y() + 0.5f));
|
||||
auto* hit = selection.get_front_object_colliding_with_in_editing_layer(clickPath);
|
||||
if(hit && hit->obj && hit->obj->get_comp().get_type() == CanvasComponentType::IMAGE)
|
||||
imageUnderCursor = hit;
|
||||
}
|
||||
if(imageUnderCursor && !selection.is_something_selected()) {
|
||||
popup_menu_action_button("Freistellen", "Freistellen", [&, imageUnderCursor] {
|
||||
process_images_on({imageUnderCursor}, true, true);
|
||||
});
|
||||
}
|
||||
if(selection.is_something_selected()) {
|
||||
bool hasImage = false;
|
||||
for(auto* c : selection.get_selection_as_set()) {
|
||||
if(c && c->obj && c->obj->get_comp().get_type() == CanvasComponentType::IMAGE) {
|
||||
hasImage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(hasImage) {
|
||||
popup_menu_action_button("Freistellen", "Freistellen", [&] {
|
||||
process_selected_images(true, true);
|
||||
});
|
||||
}
|
||||
else if(imageUnderCursor) {
|
||||
popup_menu_action_button("Freistellen", "Freistellen", [&, imageUnderCursor] {
|
||||
process_images_on({imageUnderCursor}, true, true);
|
||||
});
|
||||
}
|
||||
popup_menu_action_button("Copy", "Copy", [&] {
|
||||
selection.selection_to_clipboard();
|
||||
});
|
||||
|
|
@ -575,6 +655,31 @@ void DrawingProgram::selection_action_menu(Vector2f popupPos) {
|
|||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::request_clipboard_image_paste(Vector2f screenPos, bool applyFreistellen) {
|
||||
world.main.input.call_paste(CustomEvents::PasteEvent::DataType::IMAGE, {
|
||||
.pastePosition = screenPos,
|
||||
.applyFreistellen = applyFreistellen
|
||||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::image_paste_choice_menu(Vector2f popupPos) {
|
||||
using namespace GUIStuff;
|
||||
using namespace ElementHelpers;
|
||||
|
||||
GUIStuff::GUIManager& gui = world.main.g.gui;
|
||||
right_click_action_menu(popupPos, [&] {
|
||||
text_label_light(gui, "Paste image");
|
||||
popup_menu_action_button("Paste Image as-is", "Paste as-is", [&, popupPos] {
|
||||
selection.deselect_all();
|
||||
request_clipboard_image_paste(popupPos * world.main.g.final_gui_scale(), false);
|
||||
});
|
||||
popup_menu_action_button("Paste Image Freistellen", "Freistellen then paste", [&, popupPos] {
|
||||
selection.deselect_all();
|
||||
request_clipboard_image_paste(popupPos * world.main.g.final_gui_scale(), true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::right_click_action_menu(Vector2f popupPos, const std::function<void()>& innerContent) {
|
||||
using namespace GUIStuff;
|
||||
using namespace ElementHelpers;
|
||||
|
|
@ -642,6 +747,9 @@ void DrawingProgram::update() {
|
|||
update_downloading_dropped_files();
|
||||
check_updateable_components();
|
||||
|
||||
// After tools/transforms this frame: hide/reveal client content under moved fog.
|
||||
layerMan.flush_fog_client_projection_if_dirty();
|
||||
|
||||
if(drawCache.check_rebuild_needed_from_framerate() || drawCache.should_rebuild())
|
||||
rebuild_cache();
|
||||
}
|
||||
|
|
@ -791,7 +899,7 @@ void DrawingProgram::input_add_file_to_canvas_callback(const CustomEvents::AddFi
|
|||
|
||||
}
|
||||
|
||||
void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos) {
|
||||
void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos, bool applyFreistellen) {
|
||||
if(layerMan.is_a_layer_being_edited()) {
|
||||
ResourceData resource;
|
||||
#ifdef __ANDROID__
|
||||
|
|
@ -808,7 +916,7 @@ void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& fil
|
|||
Logger::get().log(Logger::LogType::INFO, "[DrawingProgram::add_file_to_canvas_by_path] Could not open file " + filePath.string());
|
||||
return;
|
||||
}
|
||||
ImagePreprocess::preprocess_resource(resource.name, resource.data, ImagePreprocess::options_from_config(world.main.conf));
|
||||
ImagePreprocess::preprocess_resource(resource.name, resource.data, ImagePreprocess::options_for_place(world.main.conf, applyFreistellen));
|
||||
NetworkingObjects::NetObjTemporaryPtr<ResourceData> imageTempPtr = world.rMan.add_resource(resource);
|
||||
if(imageTempPtr) {
|
||||
NetworkingObjects::NetObjID imageID = imageTempPtr.get_net_id();
|
||||
|
|
@ -826,17 +934,18 @@ void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& fil
|
|||
img.d.p2 = dropPos + imDim;
|
||||
img.d.imageID = imageID;
|
||||
auto newObjInfo = layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(newObjInfo)
|
||||
layerMan.add_undo_place_component(newObjInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos) {
|
||||
CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos, bool applyFreistellen) {
|
||||
if(layerMan.is_a_layer_being_edited()) {
|
||||
ResourceData newResource;
|
||||
newResource.data = std::make_shared<std::string>(fileBuffer);
|
||||
newResource.name = fileName;
|
||||
ImagePreprocess::preprocess_resource(newResource.name, newResource.data, ImagePreprocess::options_from_config(world.main.conf));
|
||||
ImagePreprocess::preprocess_resource(newResource.name, newResource.data, ImagePreprocess::options_for_place(world.main.conf, applyFreistellen));
|
||||
NetworkingObjects::NetObjID imageID = world.rMan.add_resource(newResource).get_net_id();
|
||||
ResourceDisplay* display = world.rMan.get_display_data(imageID);
|
||||
Vector2f imTrueDim = display->get_dimensions();
|
||||
|
|
@ -850,6 +959,8 @@ CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(co
|
|||
img.d.p2 = dropPos + imDim;
|
||||
img.d.imageID = imageID;
|
||||
CanvasComponentContainer::ObjInfo* newObjInfo = layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!newObjInfo)
|
||||
return nullptr;
|
||||
layerMan.add_undo_place_component(newObjInfo);
|
||||
return newObjInfo;
|
||||
}
|
||||
|
|
@ -858,28 +969,183 @@ CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(co
|
|||
|
||||
void DrawingProgram::process_selected_images(bool freistellen, bool compress) {
|
||||
auto selected = selection.get_selection_as_set();
|
||||
ImagePreprocess::Options opts = ImagePreprocess::options_from_config(world.main.conf);
|
||||
opts.freistellenOnInsert = freistellen;
|
||||
std::vector<CanvasComponentContainer::ObjInfo*> objs(selected.begin(), selected.end());
|
||||
process_images_on(objs, freistellen, compress);
|
||||
}
|
||||
|
||||
void DrawingProgram::process_all_canvas_images(bool freistellen, bool compress) {
|
||||
process_images_on(layerMan.get_flattened_component_list(), freistellen, compress);
|
||||
}
|
||||
|
||||
void DrawingProgram::process_images_on(const std::vector<CanvasComponentContainer::ObjInfo*>& objs, bool freistellen, bool compress) {
|
||||
ImagePreprocess::Options opts = ImagePreprocess::options_for_place(world.main.conf, freistellen);
|
||||
opts.compressOnInsert = compress;
|
||||
if(compress && opts.compressFormat == ImagePreprocess::CompressFormat::KEEP)
|
||||
opts.compressFormat = ImagePreprocess::CompressFormat::WEBP;
|
||||
if(!freistellen && !compress)
|
||||
return;
|
||||
for(auto* objInfo : selected) {
|
||||
|
||||
std::unordered_map<NetworkingObjects::NetObjID, NetworkingObjects::NetObjID> remapped;
|
||||
int changedUnique = 0;
|
||||
int changedInstances = 0;
|
||||
|
||||
for(auto* objInfo : objs) {
|
||||
if(!objInfo || !objInfo->obj)
|
||||
continue;
|
||||
if(objInfo->obj->get_comp().get_type() != CanvasComponentType::IMAGE)
|
||||
continue;
|
||||
auto& img = static_cast<ImageCanvasComponent&>(objInfo->obj->get_comp());
|
||||
auto oldId = img.d.imageID;
|
||||
auto remappedIt = remapped.find(oldId);
|
||||
if(remappedIt != remapped.end()) {
|
||||
if(remappedIt->second != oldId) {
|
||||
img.d.imageID = remappedIt->second;
|
||||
objInfo->obj->send_comp_update(*this, true);
|
||||
objInfo->obj->commit_update(*this);
|
||||
changedInstances++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
auto resource = world.netObjMan.get_obj_temporary_ref_from_id<ResourceData>(oldId);
|
||||
if(!resource || !resource->data) {
|
||||
remapped[oldId] = oldId;
|
||||
continue;
|
||||
}
|
||||
ResourceData newRes;
|
||||
newRes.name = resource->name;
|
||||
newRes.data = std::make_shared<std::string>(*resource->data);
|
||||
if(!ImagePreprocess::preprocess_resource(newRes.name, newRes.data, opts)) {
|
||||
remapped[oldId] = oldId;
|
||||
continue;
|
||||
}
|
||||
auto newId = world.rMan.add_resource(newRes).get_net_id();
|
||||
remapped[oldId] = newId;
|
||||
if(newId == oldId)
|
||||
continue;
|
||||
img.d.imageID = newId;
|
||||
objInfo->obj->send_comp_update(*this, true);
|
||||
objInfo->obj->commit_update(*this);
|
||||
changedUnique++;
|
||||
changedInstances++;
|
||||
}
|
||||
|
||||
if(changedUnique > 0)
|
||||
world.rMan.clear_display_cache();
|
||||
Logger::get().log(Logger::LogType::INFO,
|
||||
"[DrawingProgram] Image process: " + std::to_string(changedUnique) + " unique resources, " +
|
||||
std::to_string(changedInstances) + " placed images");
|
||||
}
|
||||
|
||||
DrawingProgram::ImageSizeSummaryLines DrawingProgram::canvas_image_size_summary() {
|
||||
return image_size_summary_for(layerMan.get_flattened_component_list(), canvasImageSizeCache,
|
||||
"No raster images on the canvas.",
|
||||
"On canvas now",
|
||||
"After Compress all");
|
||||
}
|
||||
|
||||
DrawingProgram::ImageSizeSummaryLines DrawingProgram::selected_image_size_summary() {
|
||||
auto selected = selection.get_selection_as_set();
|
||||
std::vector<CanvasComponentContainer::ObjInfo*> objs(selected.begin(), selected.end());
|
||||
return image_size_summary_for(objs, selectedImageSizeCache,
|
||||
"No raster images in the selection.",
|
||||
"Selected now",
|
||||
"After Compress Images");
|
||||
}
|
||||
|
||||
DrawingProgram::ImageSizeSummaryLines DrawingProgram::image_size_summary_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs, ImageSizeEstimateCache& cache, const char* emptyMsg, const char* nowPrefix, const char* afterPrefix) {
|
||||
using namespace ImagePreprocess;
|
||||
CompressFormat fmt = world.main.conf.imageCompressFormat;
|
||||
int quality = world.main.conf.imageWebpQuality;
|
||||
if(fmt == CompressFormat::KEEP)
|
||||
fmt = CompressFormat::WEBP;
|
||||
|
||||
struct UniqueRaster {
|
||||
NetworkingObjects::NetObjID id;
|
||||
std::string_view name;
|
||||
const std::string* data = nullptr;
|
||||
};
|
||||
std::vector<UniqueRaster> unique;
|
||||
unique.reserve(objs.size());
|
||||
std::unordered_set<NetworkingObjects::NetObjID> seen;
|
||||
int placed = 0;
|
||||
uint64_t fingerprint = 0;
|
||||
uint64_t currentBytes = 0;
|
||||
|
||||
for(auto* objInfo : objs) {
|
||||
if(!objInfo || !objInfo->obj)
|
||||
continue;
|
||||
if(objInfo->obj->get_comp().get_type() != CanvasComponentType::IMAGE)
|
||||
continue;
|
||||
auto& img = static_cast<ImageCanvasComponent&>(objInfo->obj->get_comp());
|
||||
auto resource = world.netObjMan.get_obj_temporary_ref_from_id<ResourceData>(img.d.imageID);
|
||||
if(!resource || !resource->data)
|
||||
if(!resource || !resource->data || resource->data->empty())
|
||||
continue;
|
||||
ResourceData newRes;
|
||||
newRes.name = resource->name;
|
||||
newRes.data = std::make_shared<std::string>(*resource->data);
|
||||
if(!ImagePreprocess::preprocess_resource(newRes.name, newRes.data, opts))
|
||||
if(!looks_like_raster_image(resource->name, *resource->data))
|
||||
continue;
|
||||
img.d.imageID = world.rMan.add_resource(newRes).get_net_id();
|
||||
objInfo->obj->send_comp_update(*this, true);
|
||||
objInfo->obj->commit_update(*this);
|
||||
placed++;
|
||||
if(!seen.insert(img.d.imageID).second)
|
||||
continue;
|
||||
UniqueRaster u;
|
||||
u.id = img.d.imageID;
|
||||
u.name = resource->name;
|
||||
u.data = resource->data.get();
|
||||
unique.push_back(u);
|
||||
currentBytes += static_cast<uint64_t>(u.data->size());
|
||||
hash_combine(fingerprint, u.id.data[0], u.id.data[1], reinterpret_cast<uintptr_t>(u.data), u.data->size());
|
||||
}
|
||||
world.rMan.clear_display_cache();
|
||||
|
||||
uint8_t fmtByte = static_cast<uint8_t>(fmt);
|
||||
bool keyChanged = fingerprint != cache.fingerprint || fmtByte != cache.format || quality != cache.quality;
|
||||
if(keyChanged) {
|
||||
cache.fingerprint = fingerprint;
|
||||
cache.format = fmtByte;
|
||||
cache.quality = quality;
|
||||
cache.placed = placed;
|
||||
cache.unique = static_cast<int>(unique.size());
|
||||
cache.currentBytes = currentBytes;
|
||||
cache.estimatedBytes = currentBytes;
|
||||
cache.estimateReady = unique.empty();
|
||||
cache.dirtySince = std::chrono::steady_clock::now();
|
||||
}
|
||||
else {
|
||||
cache.placed = placed;
|
||||
cache.unique = static_cast<int>(unique.size());
|
||||
cache.currentBytes = currentBytes;
|
||||
}
|
||||
|
||||
if(!cache.estimateReady && !unique.empty()) {
|
||||
auto elapsed = std::chrono::steady_clock::now() - cache.dirtySince;
|
||||
if(elapsed >= std::chrono::milliseconds(250)) {
|
||||
uint64_t estimated = 0;
|
||||
for(const auto& u : unique) {
|
||||
auto est = estimate_compress_size(u.name, *u.data, fmt, quality);
|
||||
if(est)
|
||||
estimated += est->estimatedBytes;
|
||||
else
|
||||
estimated += static_cast<uint64_t>(u.data->size());
|
||||
}
|
||||
cache.estimatedBytes = estimated;
|
||||
cache.estimateReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImageSizeSummaryLines out;
|
||||
if(cache.unique == 0) {
|
||||
out.nowLine = emptyMsg;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string fmtName = fmt == CompressFormat::PNG ? "PNG" : ("WebP q" + std::to_string(quality));
|
||||
out.nowLine = std::string(nowPrefix) + ": " + std::to_string(cache.unique) + " unique raster" +
|
||||
(cache.unique == 1 ? "" : "s") + " (" + std::to_string(cache.placed) + " placed) — " +
|
||||
format_byte_size(cache.currentBytes);
|
||||
out.afterLine = std::string(afterPrefix) + " (" + fmtName + "): ";
|
||||
if(cache.estimateReady)
|
||||
out.afterLine += format_byte_size(cache.estimatedBytes);
|
||||
else
|
||||
out.afterLine += "estimating…";
|
||||
return out;
|
||||
}
|
||||
|
||||
float DrawingProgram::drag_point_radius() {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
#pragma once
|
||||
#include "../DrawData.hpp"
|
||||
#include <chrono>
|
||||
#include "DrawingProgramCache.hpp"
|
||||
#include "ToolConfiguration.hpp"
|
||||
#include "Tools/GridModifyTool.hpp"
|
||||
|
|
@ -54,8 +55,8 @@ class DrawingProgram {
|
|||
void read_components_client(cereal::PortableBinaryInputArchive& a);
|
||||
void init_server_callbacks();
|
||||
void init_client_callbacks();
|
||||
void add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos);
|
||||
CanvasComponentContainer::ObjInfo* add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos);
|
||||
void add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos, bool applyFreistellen = false);
|
||||
CanvasComponentContainer::ObjInfo* add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos, bool applyFreistellen = false);
|
||||
void get_used_resources(std::unordered_set<NetworkingObjects::NetObjID>& resourceSet);
|
||||
|
||||
Vector4f* color_picker_color(Vector4f* oldColor);
|
||||
|
|
@ -82,6 +83,13 @@ class DrawingProgram {
|
|||
void send_group_ids_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs);
|
||||
void force_clients_to_own_view();
|
||||
void process_selected_images(bool freistellen, bool compress);
|
||||
void process_all_canvas_images(bool freistellen, bool compress);
|
||||
struct ImageSizeSummaryLines {
|
||||
std::string nowLine;
|
||||
std::string afterLine;
|
||||
};
|
||||
ImageSizeSummaryLines canvas_image_size_summary();
|
||||
ImageSizeSummaryLines selected_image_size_summary();
|
||||
|
||||
void on_tab_out();
|
||||
void input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile);
|
||||
|
|
@ -110,12 +118,28 @@ class DrawingProgram {
|
|||
void process_transform_message(const std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>>& transforms);
|
||||
void process_group_id_message(const std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>>& groupIds);
|
||||
void process_force_camera_jump(const CoordSpaceHelper& coords, const Vector2f& windowSize);
|
||||
void process_images_on(const std::vector<CanvasComponentContainer::ObjInfo*>& objs, bool freistellen, bool compress);
|
||||
|
||||
struct ImageSizeEstimateCache {
|
||||
uint64_t fingerprint = 0;
|
||||
uint8_t format = 255;
|
||||
int quality = -999;
|
||||
int placed = 0;
|
||||
int unique = 0;
|
||||
uint64_t currentBytes = 0;
|
||||
uint64_t estimatedBytes = 0;
|
||||
bool estimateReady = false;
|
||||
std::chrono::steady_clock::time_point dirtySince{};
|
||||
};
|
||||
ImageSizeSummaryLines image_size_summary_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs, ImageSizeEstimateCache& cache, const char* emptyMsg, const char* nowPrefix, const char* afterPrefix);
|
||||
|
||||
void drag_drop_update();
|
||||
void check_updateable_components();
|
||||
void update_downloading_dropped_files();
|
||||
|
||||
void selection_action_menu(Vector2f popupPos);
|
||||
void image_paste_choice_menu(Vector2f popupPos);
|
||||
void request_clipboard_image_paste(Vector2f screenPos, bool applyFreistellen);
|
||||
void right_click_action_menu(Vector2f popupPos, const std::function<void()>& innerContent);
|
||||
void popup_menu_action_button(const char* id, const char* text, const std::function<void()>& onClick);
|
||||
void rebuild_cache();
|
||||
|
|
@ -127,6 +151,8 @@ class DrawingProgram {
|
|||
bool is_selection_allowing_tool(DrawingProgramToolType typeToCheck);
|
||||
|
||||
DrawingProgramSelection selection;
|
||||
ImageSizeEstimateCache canvasImageSizeCache;
|
||||
ImageSizeEstimateCache selectedImageSizeCache;
|
||||
|
||||
std::unique_ptr<DrawingProgramToolBase> toolToSwitchToAfterUpdate;
|
||||
std::unordered_set<CanvasComponentContainer::ObjInfo*> updateableComponents;
|
||||
|
|
@ -159,6 +185,7 @@ class DrawingProgram {
|
|||
std::vector<DroppedDownloadingFile> droppedDownloadingFiles;
|
||||
|
||||
std::optional<Vector2f> rightClickPopupLocation;
|
||||
bool showingImagePasteChooser = false;
|
||||
|
||||
uint32_t nextID = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
#include "../MainProgram.hpp"
|
||||
#include "Helpers/Parallel.hpp"
|
||||
#include "Layers/DrawingProgramLayerManager.hpp"
|
||||
#include "Layers/SerializedBlendMode.hpp"
|
||||
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
#include <include/gpu/graphite/Surface.h>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,12 @@ void DrawingProgramSelection::selection_gui(Toolbar& t) {
|
|||
.onClick = [&] { drawP.process_selected_images(true, true); }
|
||||
});
|
||||
});
|
||||
{
|
||||
auto sizes = drawP.selected_image_size_summary();
|
||||
mutable_text_label_light(gui, "selection image size now", sizes.nowLine);
|
||||
if(!sizes.afterLine.empty())
|
||||
mutable_text_label_light(gui, "selection image size after", sizes.afterLine);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -356,7 +362,8 @@ void DrawingProgramSelection::expand_selection_with_groups() {
|
|||
return;
|
||||
std::unordered_set<CanvasComponentContainer::ObjInfo*> already(selectedSet.begin(), selectedSet.end());
|
||||
for(auto* c : drawP.layerMan.get_flattened_component_list()) {
|
||||
if(c->obj->groupId != 0 && groupIds.contains(c->obj->groupId) && !already.contains(c)) {
|
||||
if(c->obj->groupId != 0 && groupIds.contains(c->obj->groupId) && !already.contains(c)
|
||||
&& drawP.layerMan.can_current_peer_edit_layer(c->obj->parentLayer)) {
|
||||
selectedSet.push_back(c);
|
||||
already.insert(c);
|
||||
}
|
||||
|
|
@ -381,14 +388,23 @@ void DrawingProgramSelection::ungroup_selection() {
|
|||
}
|
||||
|
||||
void DrawingProgramSelection::add_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) {
|
||||
selectedSet.insert(selectedSet.end(), newSelection.begin(), newSelection.end());
|
||||
for(auto* c : newSelection) {
|
||||
if(c->obj->parentLayer && !drawP.layerMan.can_current_peer_edit_layer(c->obj->parentLayer))
|
||||
continue;
|
||||
selectedSet.emplace_back(c);
|
||||
}
|
||||
expand_selection_with_groups();
|
||||
sort_selection();
|
||||
calculate_aabb();
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::set_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) {
|
||||
selectedSet = newSelection;
|
||||
selectedSet.clear();
|
||||
for(auto* c : newSelection) {
|
||||
if(c->obj->parentLayer && !drawP.layerMan.can_current_peer_edit_layer(c->obj->parentLayer))
|
||||
continue;
|
||||
selectedSet.emplace_back(c);
|
||||
}
|
||||
expand_selection_with_groups();
|
||||
sort_selection();
|
||||
calculate_aabb();
|
||||
|
|
@ -598,11 +614,8 @@ void DrawingProgramSelection::input_key_callback_display_selection(const InputMa
|
|||
}
|
||||
case InputManager::KEY_PASTE_IMAGE: {
|
||||
if(key.down && !key.repeat) {
|
||||
deselect_all();
|
||||
drawP.world.main.input.call_paste(CustomEvents::PasteEvent::DataType::IMAGE, {
|
||||
.pastePosition = drawP.world.main.input.mouse.pos
|
||||
});
|
||||
drawP.world.main.g.gui.set_to_layout();
|
||||
drawP.showingImagePasteChooser = true;
|
||||
drawP.set_right_click_popup_location(drawP.world.main.input.mouse.pos / drawP.world.main.g.final_gui_scale());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -761,6 +774,28 @@ void DrawingProgramSelection::deselect_all() {
|
|||
}
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::remove_uneditable_from_selection() {
|
||||
if(selectedSet.empty())
|
||||
return;
|
||||
bool removed = false;
|
||||
std::erase_if(selectedSet, [&](CanvasComponentContainer::ObjInfo* c) {
|
||||
if(!c || !c->obj || drawP.layerMan.can_current_peer_edit_layer(c->obj->parentLayer))
|
||||
return false;
|
||||
drawP.drawCache.add_component(c);
|
||||
removed = true;
|
||||
return true;
|
||||
});
|
||||
if(removed) {
|
||||
if(selectedSet.empty())
|
||||
reset_all();
|
||||
else {
|
||||
sort_selection();
|
||||
calculate_aabb();
|
||||
}
|
||||
drawP.world.main.g.gui.set_to_layout();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::push_selection_to_front() {
|
||||
check_add_stroke_color_change_undo();
|
||||
|
||||
|
|
@ -874,7 +909,7 @@ void DrawingProgramSelection::paste_image_process_event(const CustomEvents::Past
|
|||
drawP.switch_to_tool(DrawingProgramToolType::EDIT);
|
||||
|
||||
std::vector<CanvasComponentContainer::ObjInfo*> compSetInserted;
|
||||
compSetInserted.emplace_back(drawP.add_file_to_canvas_by_data("Image from clipboard", paste.data, paste.mousePos.value()));
|
||||
compSetInserted.emplace_back(drawP.add_file_to_canvas_by_data("Image from clipboard", paste.data, paste.mousePos.value(), paste.applyFreistellen));
|
||||
if(compSetInserted.back() != nullptr) {
|
||||
drawP.drawCache.erase_component(compSetInserted.back());
|
||||
set_to_selection(compSetInserted);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class DrawingProgramSelection {
|
|||
void update();
|
||||
void sort_selection();
|
||||
void deselect_all();
|
||||
void remove_uneditable_from_selection();
|
||||
void paste_clipboard(Vector2f pasteScreenPos);
|
||||
void paste_image_process_event(const CustomEvents::PasteEvent& paste);
|
||||
std::unordered_set<CanvasComponentContainer::ObjInfo*> get_selection_as_set();
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ void DrawingProgramLayer::set_component_list_callbacks(DrawingProgramLayerListIt
|
|||
layerMan.drawP.drawCache.add_component(&(*c));
|
||||
if(c->obj->get_comp().get_type() == CanvasComponentType::IMAGE)
|
||||
layerMan.drawP.updateableComponents.emplace(&(*c));
|
||||
|
||||
// Live fog / lock enforcement on host
|
||||
if(layerMan.addToCacheOnComponentInsert && layerMan.drawP.world.netServer) {
|
||||
if(layerMan.drawP.world.currentNetUpdateClient && layerListItem.is_player_edit_blocked()) {
|
||||
layerMan.defer_reject_component(&layerListItem, c->obj.get_net_id());
|
||||
return;
|
||||
}
|
||||
layerMan.mark_fog_client_projection_dirty();
|
||||
}
|
||||
};
|
||||
eraseCallback = [&](const CanvasComponentContainer::ObjInfoIterator& c) {
|
||||
layerMan.drawP.selection.erase_component(&(*c));
|
||||
|
|
@ -48,6 +57,8 @@ void DrawingProgramLayer::set_component_list_callbacks(DrawingProgramLayerListIt
|
|||
return downloadingFile.comp == &(*c);
|
||||
});
|
||||
layerMan.drawP.updateableComponents.erase(&(*c));
|
||||
if(layerMan.addToCacheOnComponentInsert && layerMan.drawP.world.netServer)
|
||||
layerMan.mark_fog_client_projection_dirty();
|
||||
};
|
||||
components->set_insert_callback(insertCallback);
|
||||
components->set_erase_callback(eraseCallback);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <cereal/types/string.hpp>
|
||||
#include "DrawingProgramLayer.hpp"
|
||||
#include "../../World.hpp"
|
||||
#include "../../MainProgram.hpp"
|
||||
#include "SerializedBlendMode.hpp"
|
||||
|
||||
using namespace NetworkingObjects;
|
||||
|
|
@ -78,6 +79,8 @@ DrawingProgramLayerListItem::DrawingProgramLayerListItem(World& w, const Drawing
|
|||
displayData = w.netObjMan.make_obj<DisplayData>();
|
||||
displayData->alpha = initData.metaInfo.alpha;
|
||||
displayData->blendMode = initData.metaInfo.blendMode;
|
||||
displayData->playerLocked = initData.metaInfo.playerLocked;
|
||||
displayData->fogLayer = initData.metaInfo.fogLayer;
|
||||
}
|
||||
|
||||
DrawingProgramLayerListItemUndoData DrawingProgramLayerListItem::get_undo_data(WorldUndoManager& u) const {
|
||||
|
|
@ -85,6 +88,8 @@ DrawingProgramLayerListItemUndoData DrawingProgramLayerListItem::get_undo_data(W
|
|||
toRet.metaInfo.name = nameData->name;
|
||||
toRet.metaInfo.blendMode = displayData->blendMode;
|
||||
toRet.metaInfo.alpha = displayData->alpha;
|
||||
toRet.metaInfo.playerLocked = displayData->playerLocked;
|
||||
toRet.metaInfo.fogLayer = displayData->fogLayer;
|
||||
if(folderData) {
|
||||
toRet.containerUndoID = u.get_undoid_from_netid(folderData->folderList.get_net_id());
|
||||
toRet.folderData = std::vector<DrawingProgramLayerListItemUndoData>();
|
||||
|
|
@ -178,11 +183,24 @@ void DrawingProgramLayerListItem::load_file(cereal::PortableBinaryInputArchive&
|
|||
a(*nameData);
|
||||
|
||||
displayData = layerMan.drawP.world.netObjMan.make_obj<DisplayData>();
|
||||
if(version >= VersionNumber(0, 8, 0)) {
|
||||
a(*displayData);
|
||||
// Normalize cereal bools (non-0/1 bytes must not stick as "true" forever).
|
||||
displayData->visible = displayData->visible ? true : false;
|
||||
displayData->playerLocked = displayData->playerLocked ? true : false;
|
||||
displayData->fogLayer = displayData->fogLayer ? true : false;
|
||||
}
|
||||
else {
|
||||
a(displayData->alpha, displayData->visible, displayData->blendMode);
|
||||
displayData->visible = displayData->visible ? true : false;
|
||||
displayData->playerLocked = false;
|
||||
displayData->fogLayer = false;
|
||||
}
|
||||
|
||||
bool isFolder;
|
||||
a(isFolder);
|
||||
if(isFolder) {
|
||||
displayData->fogLayer = false;
|
||||
folderData = std::make_unique<DrawingProgramLayerFolder>();
|
||||
folderData->load_file(a, version, layerMan);
|
||||
}
|
||||
|
|
@ -275,20 +293,84 @@ SerializedBlendMode DrawingProgramLayerListItem::get_blend_mode() const {
|
|||
return displayData->blendMode;
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::set_player_locked(DrawingProgramLayerManager& layerMan, bool locked) const {
|
||||
if(!displayData)
|
||||
return;
|
||||
bool newLocked = locked || displayData->fogLayer;
|
||||
if(displayData->playerLocked != newLocked) {
|
||||
displayData->playerLocked = newLocked;
|
||||
layerMan.drawP.drawCache.clear_own_cached_surfaces();
|
||||
layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all<DisplayData>(displayData, false);
|
||||
layerMan.refresh_fog_client_projection();
|
||||
}
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerListItem::get_player_locked() const {
|
||||
return displayData && displayData->playerLocked;
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::set_fog_layer(DrawingProgramLayerManager& layerMan, bool fog) const {
|
||||
if(!displayData || displayData->fogLayer == fog || is_folder())
|
||||
return;
|
||||
displayData->fogLayer = fog;
|
||||
if(fog)
|
||||
displayData->playerLocked = true;
|
||||
layerMan.drawP.drawCache.clear_own_cached_surfaces();
|
||||
layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all<DisplayData>(displayData, false);
|
||||
layerMan.refresh_fog_client_projection();
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerListItem::is_fog_layer() const {
|
||||
return displayData && displayData->fogLayer;
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerListItem::is_player_edit_blocked() const {
|
||||
return is_fog_layer() || get_player_locked();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::set_metainfo(DrawingProgramLayerManager& layerMan, const DrawingProgramLayerListItemMetaInfo& metaInfo) {
|
||||
set_blend_mode(layerMan, metaInfo.blendMode);
|
||||
set_alpha(layerMan, metaInfo.alpha);
|
||||
set_name(layerMan.drawP.world.delayedUpdateObjectManager, metaInfo.name);
|
||||
set_fog_layer(layerMan, metaInfo.fogLayer);
|
||||
set_player_locked(layerMan, metaInfo.playerLocked || metaInfo.fogLayer);
|
||||
}
|
||||
|
||||
DrawingProgramLayerListItemMetaInfo DrawingProgramLayerListItem::get_metainfo() const {
|
||||
return DrawingProgramLayerListItemMetaInfo{
|
||||
.name = get_name(),
|
||||
.alpha = get_alpha(),
|
||||
.blendMode = get_blend_mode()
|
||||
.blendMode = get_blend_mode(),
|
||||
.playerLocked = get_player_locked(),
|
||||
.fogLayer = is_fog_layer()
|
||||
};
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::write_stripped_constructor(cereal::PortableBinaryOutputArchive& a, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds) const {
|
||||
a(static_cast<bool>(folderData));
|
||||
if(folderData) {
|
||||
a(folderData->folderList.get_net_id());
|
||||
a(static_cast<uint32_t>(folderData->folderList->size()));
|
||||
for(auto& child : *folderData->folderList) {
|
||||
a(child.obj.get_net_id());
|
||||
child.obj->write_stripped_constructor(a, hiddenComponentIds);
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::vector<NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer>> keep;
|
||||
for(auto& c : *layerData->components) {
|
||||
if(!hiddenComponentIds.contains(c.obj.get_net_id()))
|
||||
keep.emplace_back(c.obj);
|
||||
}
|
||||
a(layerData->components.get_net_id());
|
||||
a(static_cast<uint32_t>(keep.size()));
|
||||
for(auto& c : keep)
|
||||
c.write_create_message(a);
|
||||
}
|
||||
nameData.write_create_message(a);
|
||||
displayData.write_create_message(a);
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::erase_invalid_components() {
|
||||
if(folderData)
|
||||
folderData->erase_invalid_components();
|
||||
|
|
@ -312,7 +394,9 @@ void DrawingProgramLayerListItem::register_class(World& w) {
|
|||
});
|
||||
w.delayedUpdateObjectManager.register_class<DisplayData>(w.netObjMan, NetworkingObjects::DelayUpdateSerializedClassManager::CustomConstructors<DisplayData>{
|
||||
.postUpdateFunc = [&](DisplayData& o) {
|
||||
(void)o;
|
||||
w.drawProg.drawCache.clear_own_cached_surfaces();
|
||||
w.drawProg.layerMan.ensure_editing_layer_is_editable();
|
||||
w.set_to_layout_gui_if_focus();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ struct DrawingProgramLayerListItemMetaInfo {
|
|||
std::string name;
|
||||
float alpha = 1.0f;
|
||||
SerializedBlendMode blendMode = SerializedBlendMode::BLEND_SRC_OVER;
|
||||
bool playerLocked = false;
|
||||
bool fogLayer = false;
|
||||
bool operator==(const DrawingProgramLayerListItemMetaInfo&) const = default;
|
||||
};
|
||||
|
||||
|
|
@ -80,12 +82,22 @@ class DrawingProgramLayerListItem {
|
|||
void set_blend_mode(DrawingProgramLayerManager& layerMan, SerializedBlendMode newBlendMode) const;
|
||||
SerializedBlendMode get_blend_mode() const;
|
||||
|
||||
void set_player_locked(DrawingProgramLayerManager& layerMan, bool locked) const;
|
||||
bool get_player_locked() const;
|
||||
void set_fog_layer(DrawingProgramLayerManager& layerMan, bool fog) const;
|
||||
bool is_fog_layer() const;
|
||||
// Fog layers are always treated as player-locked for edit checks.
|
||||
bool is_player_edit_blocked() const;
|
||||
|
||||
void set_metainfo(DrawingProgramLayerManager& layerMan, const DrawingProgramLayerListItemMetaInfo& metaInfo);
|
||||
DrawingProgramLayerListItemMetaInfo get_metainfo() const;
|
||||
|
||||
uint32_t get_component_count() const;
|
||||
|
||||
void erase_invalid_components();
|
||||
|
||||
// Host→client join projection: same shape as write_constructor_func, omits hidden comps.
|
||||
void write_stripped_constructor(cereal::PortableBinaryOutputArchive& a, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds) const;
|
||||
private:
|
||||
static void write_constructor_func(const NetworkingObjects::NetObjTemporaryPtr<DrawingProgramLayerListItem>& o, cereal::PortableBinaryOutputArchive& a);
|
||||
static void read_constructor_func(const NetworkingObjects::NetObjTemporaryPtr<DrawingProgramLayerListItem>& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr<NetServer::ClientData>& c);
|
||||
|
|
@ -101,8 +113,10 @@ class DrawingProgramLayerListItem {
|
|||
float alpha = 1.0f;
|
||||
bool visible = true;
|
||||
SerializedBlendMode blendMode = SerializedBlendMode::BLEND_SRC_OVER;
|
||||
bool playerLocked = false;
|
||||
bool fogLayer = false;
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a(alpha, visible, blendMode);
|
||||
a(alpha, visible, blendMode, playerLocked, fogLayer);
|
||||
}
|
||||
};
|
||||
NetworkingObjects::NetObjOwnerPtr<NameData> nameData;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
#include "DrawingProgramLayerManager.hpp"
|
||||
#include "FogStripper.hpp"
|
||||
#include "../../World.hpp"
|
||||
#include "Helpers/NetworkingObjects/NetObjID.hpp"
|
||||
#include "Helpers/NetworkingObjects/NetObjOrderedList.hpp"
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
#include "../../CanvasComponents/CanvasComponentAllocator.hpp"
|
||||
#include "../../CanvasComponents/CanvasComponent.hpp"
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
|
||||
DrawingProgramLayerManager::DrawingProgramLayerManager(DrawingProgram& drawProg):
|
||||
drawP(drawProg), // initialize drawP first for next objects to be initialized properly
|
||||
|
|
@ -44,7 +46,10 @@ void DrawingProgramLayerManager::scale_up(const WorldScalar& scaleUpAmount) {
|
|||
}
|
||||
|
||||
void DrawingProgramLayerManager::write_components_server(cereal::PortableBinaryOutputArchive& a) {
|
||||
layerTreeRoot.write_create_message(a);
|
||||
auto hidden = FogStripper::compute_hidden_component_ids(*this);
|
||||
clientHiddenComponentIds = hidden;
|
||||
a(layerTreeRoot.get_net_id());
|
||||
layerTreeRoot->write_stripped_constructor(a, hidden);
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::read_components_client(cereal::PortableBinaryInputArchive& a) {
|
||||
|
|
@ -58,6 +63,7 @@ void DrawingProgramLayerManager::read_components_client(cereal::PortableBinaryIn
|
|||
});
|
||||
drawP.rebuild_cache();
|
||||
editingLayer = layerTreeRoot->get_folder().get_initial_editing_layer();
|
||||
ensure_editing_layer_is_editable();
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerManager::is_a_layer_being_edited() {
|
||||
|
|
@ -104,6 +110,10 @@ const DrawingProgramLayerListItem& DrawingProgramLayerManager::get_layer_root()
|
|||
}
|
||||
|
||||
bool DrawingProgramLayerManager::component_passes_layer_selector(CanvasComponentContainer::ObjInfo* c, LayerSelector layerSelector) {
|
||||
if(!c || !c->obj || !c->obj->parentLayer)
|
||||
return false;
|
||||
if(!can_current_peer_edit_layer(c->obj->parentLayer))
|
||||
return false;
|
||||
switch(layerSelector) {
|
||||
case LayerSelector::ALL_VISIBLE_LAYERS:
|
||||
return c->obj->parentLayer->get_visible();
|
||||
|
|
@ -243,6 +253,11 @@ std::vector<CanvasComponentContainer::ObjInfoIterator> DrawingProgramLayerManage
|
|||
auto editLayerPtr = editingLayer.lock();
|
||||
if(!editLayerPtr)
|
||||
throw std::runtime_error("[DrawingProgramLayerManager::add_many_components_to_layer_being_edited] No layer selected!");
|
||||
if(!can_current_peer_edit_layer(editLayerPtr.get())) {
|
||||
for(auto& [it, obj] : newObjs)
|
||||
delete obj;
|
||||
return {};
|
||||
}
|
||||
auto toRet = editLayerPtr->get_layer().components->insert_sorted_list_and_send_create(editLayerPtr->get_layer().components, newObjs);
|
||||
add_undo_place_components(editingLayer.lock().get(), toRet);
|
||||
return toRet;
|
||||
|
|
@ -276,10 +291,90 @@ void DrawingProgramLayerManager::disable_add_to_cache_and_commit_update_block(co
|
|||
addToCacheOnComponentInsert = true;
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerManager::can_current_peer_edit_layer(const DrawingProgramLayerListItem* layer) const {
|
||||
if(!layer)
|
||||
return false;
|
||||
if(FogStripper::world_is_host(drawP.world))
|
||||
return true;
|
||||
return !layer->is_player_edit_blocked();
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerManager::client_may_modify_component(const CanvasComponentContainer* comp) const {
|
||||
// Host-side authority check for CLIENT messages — must NOT use can_current_peer_edit_layer
|
||||
// (that always returns true on the host).
|
||||
if(!comp || !comp->parentLayer)
|
||||
return false;
|
||||
return !comp->parentLayer->is_player_edit_blocked();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::ensure_editing_layer_is_editable() {
|
||||
auto cur = editingLayer.lock();
|
||||
if(cur && can_current_peer_edit_layer(cur.get()))
|
||||
return;
|
||||
if(!layerTreeRoot)
|
||||
return;
|
||||
std::function<NetworkingObjects::NetObjWeakPtr<DrawingProgramLayerListItem>(const DrawingProgramLayerFolder&)> findEditable;
|
||||
findEditable = [&](const DrawingProgramLayerFolder& folder) -> NetworkingObjects::NetObjWeakPtr<DrawingProgramLayerListItem> {
|
||||
for(auto& c : *folder.folderList) {
|
||||
if(!c.obj->is_folder() && can_current_peer_edit_layer(c.obj.get()))
|
||||
return c.obj;
|
||||
}
|
||||
for(auto& c : *folder.folderList) {
|
||||
if(c.obj->is_folder()) {
|
||||
auto found = findEditable(c.obj->get_folder());
|
||||
if(!found.expired())
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
editingLayer = findEditable(layerTreeRoot->get_folder());
|
||||
drawP.selection.remove_uneditable_from_selection();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::refresh_fog_client_projection() {
|
||||
fogClientProjectionDirty = false;
|
||||
FogStripper::sync_hidden_set_to_clients(drawP.world, *this);
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::mark_fog_client_projection_dirty() {
|
||||
if(drawP.world.netServer)
|
||||
fogClientProjectionDirty = true;
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::flush_fog_client_projection_if_dirty() {
|
||||
if(!fogClientProjectionDirty)
|
||||
return;
|
||||
refresh_fog_client_projection();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::defer_reject_component(DrawingProgramLayerListItem* layer, NetworkingObjects::NetObjID compId) {
|
||||
deferredRejectInserts.emplace_back(layer, compId);
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::flush_deferred_rejects() {
|
||||
if(deferredRejectInserts.empty())
|
||||
return;
|
||||
auto pending = std::move(deferredRejectInserts);
|
||||
deferredRejectInserts.clear();
|
||||
for(auto& [layer, compId] : pending) {
|
||||
if(!layer || layer->is_folder())
|
||||
continue;
|
||||
auto& comps = layer->get_layer().components;
|
||||
if(!comps->contains(compId))
|
||||
continue;
|
||||
comps->erase(comps, comps->get(compId));
|
||||
}
|
||||
}
|
||||
|
||||
CanvasComponentContainer::ObjInfo* DrawingProgramLayerManager::add_component_to_layer_being_edited(CanvasComponentContainer* newObj) {
|
||||
auto editLayerPtr = editingLayer.lock();
|
||||
if(!editLayerPtr)
|
||||
throw std::runtime_error("[DrawingProgramLayerManager::add_component_to_layer_being_edited] No layer selected!");
|
||||
if(!can_current_peer_edit_layer(editLayerPtr.get())) {
|
||||
delete newObj;
|
||||
return nullptr;
|
||||
}
|
||||
return &(*editLayerPtr->get_layer().components->push_back_and_send_create(editLayerPtr->get_layer().components, newObj));
|
||||
}
|
||||
|
||||
|
|
@ -311,6 +406,7 @@ void DrawingProgramLayerManager::load_file(cereal::PortableBinaryInputArchive& a
|
|||
layerTreeRoot->erase_invalid_components();
|
||||
drawP.rebuild_cache();
|
||||
editingLayer = layerTreeRoot->get_folder().get_initial_editing_layer();
|
||||
ensure_editing_layer_is_editable();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::save_file(cereal::PortableBinaryOutputArchive& a) const {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include "DrawingProgramLayerListItem.hpp"
|
||||
#include "DrawingProgramLayerManagerGUI.hpp"
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
class DrawingProgramLayerManager {
|
||||
private:
|
||||
|
|
@ -85,5 +86,25 @@ class DrawingProgramLayerManager {
|
|||
std::vector<CanvasComponentContainer::ObjInfo*> get_flattened_component_list() const;
|
||||
std::vector<DrawingProgramLayerListItem*> get_flattened_layer_list();
|
||||
void add_undo_place_component(CanvasComponentContainer::ObjInfo* objInfo);
|
||||
|
||||
bool can_current_peer_edit_layer(const DrawingProgramLayerListItem* layer) const;
|
||||
bool client_may_modify_component(const CanvasComponentContainer* comp) const;
|
||||
void ensure_editing_layer_is_editable();
|
||||
/** Host: recompute fog coverage and erase/reveal (+ resources) on clients. */
|
||||
void refresh_fog_client_projection();
|
||||
/** Coalesce frequent mesh edits; flushed once per frame in DrawingProgram::update. */
|
||||
void mark_fog_client_projection_dirty();
|
||||
void flush_fog_client_projection_if_dirty();
|
||||
|
||||
// NetObjIDs omitted from the last client projection (host only).
|
||||
std::unordered_set<NetworkingObjects::NetObjID> clientHiddenComponentIds;
|
||||
|
||||
// Client inserts into locked/fog layers — erase after the NetObj update finishes.
|
||||
void defer_reject_component(DrawingProgramLayerListItem* layer, NetworkingObjects::NetObjID compId);
|
||||
void flush_deferred_rejects();
|
||||
|
||||
DrawingProgramLayerManagerGUI listGUI;
|
||||
private:
|
||||
std::vector<std::pair<DrawingProgramLayerListItem*, NetworkingObjects::NetObjID>> deferredRejectInserts;
|
||||
bool fogClientProjectionDirty = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include "DrawingProgramLayerManagerGUI.hpp"
|
||||
#include "DrawingProgramLayerListItem.hpp"
|
||||
#include "DrawingProgramLayerManager.hpp"
|
||||
#include "FogStripper.hpp"
|
||||
#include "../DrawingProgram.hpp"
|
||||
#include "../../World.hpp"
|
||||
#include "../../MainProgram.hpp"
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
#include "../../GUIStuff/ElementHelpers/LayoutHelpers.hpp"
|
||||
#include "../../GUIStuff/ElementHelpers/TextBoxHelpers.hpp"
|
||||
#include "../../GUIStuff/ElementHelpers/NumberSliderHelpers.hpp"
|
||||
#include "../../GUIStuff/ElementHelpers/CheckBoxHelpers.hpp"
|
||||
#include "../../GUIStuff/Elements/SVGIcon.hpp"
|
||||
#include "../../GUIStuff/Elements/DropDown.hpp"
|
||||
|
||||
|
|
@ -122,14 +124,20 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() {
|
|||
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}
|
||||
},
|
||||
}) {
|
||||
ellipse_wide_paragraph_label(gui, "layer name", layer->get_name());
|
||||
std::string label = layer->get_name();
|
||||
if(layer->is_fog_layer())
|
||||
label += " [fog]";
|
||||
else if(layer->is_player_edit_blocked())
|
||||
label += " [lock]";
|
||||
ellipse_wide_paragraph_label(gui, "layer name", label);
|
||||
}
|
||||
if(!layer->is_folder()) {
|
||||
if(!layer->is_folder() && layerMan.can_current_peer_edit_layer(layer.get())) {
|
||||
gui.set_z_index_keep_clipping_region(gui.get_z_index() + 1, [&] {
|
||||
svg_icon_button(gui, "edit button", "data/icons/pencil.svg", {
|
||||
.drawType = SelectableButton::DrawType::TRANSPARENT_ALL,
|
||||
.size = TreeListing::ENTRY_HEIGHT,
|
||||
.onClick = [&, layer] {
|
||||
if(layerMan.can_current_peer_edit_layer(layer.get()))
|
||||
layerMan.editingLayer = layer;
|
||||
}
|
||||
});
|
||||
|
|
@ -155,8 +163,11 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() {
|
|||
});
|
||||
},
|
||||
.onDoubleClick = [&](const TreeListingObjIndexList& objIndex) {
|
||||
layerMan.editingLayer = get_layer_from_obj_index(objIndex);
|
||||
auto layer = get_layer_from_obj_index(objIndex);
|
||||
if(layerMan.can_current_peer_edit_layer(layer.get())) {
|
||||
layerMan.editingLayer = layer;
|
||||
layerMan.drawP.world.main.g.gui.set_to_layout();
|
||||
}
|
||||
},
|
||||
.onSelectChange = [&] {
|
||||
editing_layer_check();
|
||||
|
|
@ -441,6 +452,34 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() {
|
|||
}
|
||||
});
|
||||
});
|
||||
if(FogStripper::world_is_host(world) && !editingLayerLock->is_folder()) {
|
||||
// Must not use stack bool* with checkbox_boolean_field — CheckBox
|
||||
// calls isTicked/onClick after layout returns (UAF → random fog flags).
|
||||
checkbox_field(gui, "fog layer toggle", "Fog of War layer",
|
||||
[&] {
|
||||
auto layer = editingLayer.lock();
|
||||
return layer && layer->is_fog_layer();
|
||||
},
|
||||
[&] {
|
||||
auto layer = editingLayer.lock();
|
||||
if(layer)
|
||||
layer->set_fog_layer(layerMan, !layer->is_fog_layer());
|
||||
});
|
||||
if(!editingLayerLock->is_fog_layer()) {
|
||||
checkbox_field(gui, "player lock toggle", "Lock for players",
|
||||
[&] {
|
||||
auto layer = editingLayer.lock();
|
||||
return layer && layer->get_player_locked();
|
||||
},
|
||||
[&] {
|
||||
auto layer = editingLayer.lock();
|
||||
if(layer)
|
||||
layer->set_player_locked(layerMan, !layer->get_player_locked());
|
||||
});
|
||||
}
|
||||
else
|
||||
text_label(gui, "Fog layers are always locked for players.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
258
src/DrawingProgram/Layers/FogStripper.cpp
Normal file
258
src/DrawingProgram/Layers/FogStripper.cpp
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* InfiniPaint
|
||||
* Copyright (C) 2025-2026 Yousef Khadadeh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FogStripper.hpp"
|
||||
#include "DrawingProgramLayerManager.hpp"
|
||||
#include "DrawingProgramLayerListItem.hpp"
|
||||
#include "../../World.hpp"
|
||||
#include "../../CommandList.hpp"
|
||||
#include "../../CanvasComponents/MeshCanvasComponent.hpp"
|
||||
#include "../../CanvasComponents/RectangleCanvasComponent.hpp"
|
||||
#include "../../CanvasComponents/EllipseCanvasComponent.hpp"
|
||||
#include <Helpers/NetworkingObjects/NetObjOrderedList.hpp>
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <include/core/SkPath.h>
|
||||
#include <include/core/SkPathBuilder.h>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace FogStripper {
|
||||
namespace {
|
||||
|
||||
struct FogCoverPiece {
|
||||
CanvasComponentContainer* fogComp = nullptr;
|
||||
float effectiveAlpha = 1.0f;
|
||||
};
|
||||
|
||||
float component_paint_alpha(const CanvasComponentContainer& c) {
|
||||
switch(c.get_comp().get_type()) {
|
||||
case CanvasComponentType::MESH:
|
||||
return static_cast<const MeshCanvasComponent&>(c.get_comp()).d.color.w();
|
||||
case CanvasComponentType::RECTANGLE: {
|
||||
auto& r = static_cast<const RectangleCanvasComponent&>(c.get_comp());
|
||||
return std::max(r.d.fillColor.w(), r.d.strokeColor.w());
|
||||
}
|
||||
case CanvasComponentType::ELLIPSE: {
|
||||
auto& e = static_cast<const EllipseCanvasComponent&>(c.get_comp());
|
||||
return std::max(e.d.fillColor.w(), e.d.strokeColor.w());
|
||||
}
|
||||
default:
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
bool point_inside_fog(CanvasComponentContainer& fogComp, const WorldVec& worldPoint) {
|
||||
// collides_with_point expects the probe in fogComp.coords space.
|
||||
Vector2f local = fogComp.coords.to_space(worldPoint);
|
||||
return fogComp.collides_with_point(fogComp.coords, local);
|
||||
}
|
||||
|
||||
bool victim_fully_covered_by_fog(CanvasComponentContainer& victim, const FogCoverPiece& fogPiece) {
|
||||
if(!victim.get_world_bounds().has_value() || !fogPiece.fogComp->get_world_bounds().has_value())
|
||||
return false;
|
||||
if(fogPiece.effectiveAlpha < OPAQUE_ALPHA_THRESHOLD)
|
||||
return false;
|
||||
|
||||
const auto& victimAABB = victim.get_world_bounds().value();
|
||||
const auto& fogAABB = fogPiece.fogComp->get_world_bounds().value();
|
||||
// Must be completely inside the fog piece, not merely overlapping.
|
||||
if(!fogAABB.fully_contains_aabb(victimAABB))
|
||||
return false;
|
||||
|
||||
// Confirm shape coverage (brush blobs are not the AABB): corners + center inside fog paint.
|
||||
const WorldVec corners[5] = {
|
||||
victimAABB.min,
|
||||
victimAABB.max,
|
||||
victimAABB.top_right(),
|
||||
victimAABB.bottom_left(),
|
||||
victimAABB.center()
|
||||
};
|
||||
for(const auto& corner : corners) {
|
||||
if(!point_inside_fog(*fogPiece.fogComp, corner))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool world_is_host(const World& world) {
|
||||
return world.netServer != nullptr || world.netClient == nullptr;
|
||||
}
|
||||
|
||||
std::unordered_set<NetworkingObjects::NetObjID> compute_hidden_component_ids(DrawingProgramLayerManager& layerMan) {
|
||||
std::unordered_set<NetworkingObjects::NetObjID> hidden;
|
||||
if(!layerMan.layer_tree_root_exists())
|
||||
return hidden;
|
||||
|
||||
std::vector<DrawingProgramLayerListItem*> layers = layerMan.get_flattened_layer_list();
|
||||
// Flattened forward order: index 0 is top of stack (drawn last). Fog at i covers layers j > i.
|
||||
std::vector<FogCoverPiece> activeFog;
|
||||
|
||||
for(size_t i = 0; i < layers.size(); ++i) {
|
||||
DrawingProgramLayerListItem* layer = layers[i];
|
||||
if(layer->is_fog_layer() && layer->get_visible()) {
|
||||
float layerAlpha = layer->get_alpha();
|
||||
for(auto& c : *layer->get_layer().components) {
|
||||
FogCoverPiece piece;
|
||||
piece.fogComp = c.obj.get();
|
||||
piece.effectiveAlpha = layerAlpha * component_paint_alpha(*c.obj);
|
||||
if(piece.effectiveAlpha >= OPAQUE_ALPHA_THRESHOLD)
|
||||
activeFog.emplace_back(piece);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(activeFog.empty())
|
||||
continue;
|
||||
|
||||
for(auto& c : *layer->get_layer().components) {
|
||||
for(const auto& fogPiece : activeFog) {
|
||||
if(victim_fully_covered_by_fog(*c.obj, fogPiece)) {
|
||||
hidden.insert(c.obj.get_net_id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hidden;
|
||||
}
|
||||
|
||||
std::unordered_set<NetworkingObjects::NetObjID> compute_visible_resource_ids(DrawingProgramLayerManager& layerMan, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds) {
|
||||
std::unordered_set<NetworkingObjects::NetObjID> resources;
|
||||
auto comps = layerMan.get_flattened_component_list();
|
||||
for(auto* info : comps) {
|
||||
if(hiddenComponentIds.contains(info->obj.get_net_id()))
|
||||
continue;
|
||||
info->obj->get_comp().get_used_resources(resources);
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
void write_stripped_layer_tree(const DrawingProgramLayerListItem& root, cereal::PortableBinaryOutputArchive& a, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds) {
|
||||
root.write_stripped_constructor(a, hiddenComponentIds);
|
||||
}
|
||||
|
||||
void sync_hidden_set_to_clients(World& world, DrawingProgramLayerManager& layerMan) {
|
||||
if(!world.netServer)
|
||||
return;
|
||||
|
||||
auto newHidden = compute_hidden_component_ids(layerMan);
|
||||
auto& prevHidden = layerMan.clientHiddenComponentIds;
|
||||
|
||||
std::vector<NetworkingObjects::NetObjID> newlyHidden;
|
||||
std::vector<NetworkingObjects::NetObjID> newlyRevealed;
|
||||
for(const auto& id : newHidden) {
|
||||
if(!prevHidden.contains(id))
|
||||
newlyHidden.push_back(id);
|
||||
}
|
||||
for(const auto& id : prevHidden) {
|
||||
if(!newHidden.contains(id))
|
||||
newlyRevealed.push_back(id);
|
||||
}
|
||||
|
||||
if(!newlyHidden.empty()) {
|
||||
std::unordered_map<NetworkingObjects::NetObjID, std::vector<NetworkingObjects::NetObjID>> eraseByList;
|
||||
auto comps = layerMan.get_flattened_component_list();
|
||||
for(auto* info : comps) {
|
||||
NetworkingObjects::NetObjID compId = info->obj.get_net_id();
|
||||
if(std::find(newlyHidden.begin(), newlyHidden.end(), compId) == newlyHidden.end())
|
||||
continue;
|
||||
auto* parent = info->obj->parentLayer;
|
||||
if(!parent || parent->is_folder())
|
||||
continue;
|
||||
eraseByList[parent->get_layer().components.get_net_id()].push_back(compId);
|
||||
}
|
||||
for(auto& [listId, ids] : eraseByList) {
|
||||
auto listPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer::NetList>(listId);
|
||||
if(!listPtr)
|
||||
continue;
|
||||
listPtr.send_server_update_to_all_clients(RELIABLE_COMMAND_CHANNEL, [&ids](const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer::NetList>&, cereal::PortableBinaryOutputArchive& a) {
|
||||
a(NetworkingObjects::ObjPtrOrderedListCommand_StoC::ERASE_ID_LIST, ids);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(!newlyRevealed.empty()) {
|
||||
struct RevealItem {
|
||||
NetworkingObjects::NetObjID listId;
|
||||
NetworkingObjects::NetObjID afterId;
|
||||
NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer> obj;
|
||||
};
|
||||
std::vector<RevealItem> reveals;
|
||||
auto comps = layerMan.get_flattened_component_list();
|
||||
for(auto* info : comps) {
|
||||
NetworkingObjects::NetObjID compId = info->obj.get_net_id();
|
||||
if(std::find(newlyRevealed.begin(), newlyRevealed.end(), compId) == newlyRevealed.end())
|
||||
continue;
|
||||
auto* parent = info->obj->parentLayer;
|
||||
if(!parent || parent->is_folder())
|
||||
continue;
|
||||
RevealItem r;
|
||||
r.listId = parent->get_layer().components.get_net_id();
|
||||
r.obj = info->obj;
|
||||
r.afterId = NetworkingObjects::NetObjID{0, 0};
|
||||
if(info->pos > 0) {
|
||||
auto prevIt = parent->get_layer().components->at(info->pos - 1);
|
||||
r.afterId = prevIt->obj.get_net_id();
|
||||
while(prevHidden.contains(r.afterId) || newHidden.contains(r.afterId)) {
|
||||
if(prevIt->pos == 0) {
|
||||
r.afterId = NetworkingObjects::NetObjID{0, 0};
|
||||
break;
|
||||
}
|
||||
prevIt = parent->get_layer().components->at(prevIt->pos - 1);
|
||||
r.afterId = prevIt->obj.get_net_id();
|
||||
}
|
||||
}
|
||||
reveals.push_back(r);
|
||||
}
|
||||
|
||||
std::unordered_map<NetworkingObjects::NetObjID, std::vector<RevealItem>> byList;
|
||||
for(auto& r : reveals)
|
||||
byList[r.listId].push_back(r);
|
||||
|
||||
for(auto& [listId, items] : byList) {
|
||||
auto listPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer::NetList>(listId);
|
||||
if(!listPtr)
|
||||
continue;
|
||||
listPtr.send_server_update_to_all_clients(RELIABLE_COMMAND_CHANNEL, [&items](const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer::NetList>&, cereal::PortableBinaryOutputArchive& a) {
|
||||
a(NetworkingObjects::ObjPtrOrderedListCommand_StoC::INSERT_LIST_CONSTRUCT, static_cast<uint32_t>(items.size()));
|
||||
for(auto& item : items) {
|
||||
a(item.afterId);
|
||||
item.obj.write_create_message(a);
|
||||
}
|
||||
});
|
||||
std::unordered_set<NetworkingObjects::NetObjID> needed;
|
||||
for(auto& item : items)
|
||||
item.obj->get_comp().get_used_resources(needed);
|
||||
for(const auto& resId : needed) {
|
||||
for(auto& r : world.rMan.resource_list()) {
|
||||
if(r.get_net_id() == resId) {
|
||||
world.netServer->send_items_to_all_clients(RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE_ID, resId);
|
||||
world.netServer->send_items_to_all_clients(RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE_DATA, *r);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevHidden = std::move(newHidden);
|
||||
}
|
||||
|
||||
} // namespace FogStripper
|
||||
45
src/DrawingProgram/Layers/FogStripper.hpp
Normal file
45
src/DrawingProgram/Layers/FogStripper.hpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* InfiniPaint
|
||||
* Copyright (C) 2025-2026 Yousef Khadadeh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Helpers/NetworkingObjects/NetObjID.hpp>
|
||||
#include <cereal/archives/portable_binary.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
class DrawingProgramLayerManager;
|
||||
class DrawingProgramLayerListItem;
|
||||
class World;
|
||||
|
||||
// Host-side projection only. Never mutates the host world graph.
|
||||
namespace FogStripper {
|
||||
constexpr float OPAQUE_ALPHA_THRESHOLD = 0.95f;
|
||||
|
||||
bool world_is_host(const World& world);
|
||||
|
||||
// Component NetObjIDs that must be omitted from player join/live views.
|
||||
std::unordered_set<NetworkingObjects::NetObjID> compute_hidden_component_ids(DrawingProgramLayerManager& layerMan);
|
||||
|
||||
// Resources still referenced by the player-visible projection (not hidden, plus fog paint).
|
||||
std::unordered_set<NetworkingObjects::NetObjID> compute_visible_resource_ids(DrawingProgramLayerManager& layerMan, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds);
|
||||
|
||||
// Write layerTreeRoot create message with hidden components omitted from leaf lists.
|
||||
void write_stripped_layer_tree(const DrawingProgramLayerListItem& root, cereal::PortableBinaryOutputArchive& a, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds);
|
||||
|
||||
// After fog geometry/flags change: erase newly hidden IDs from clients; insert newly revealed ones.
|
||||
void sync_hidden_set_to_clients(World& world, DrawingProgramLayerManager& layerMan);
|
||||
}
|
||||
|
|
@ -68,6 +68,8 @@ void BrushTool::input_mouse_button_on_canvas_callback(const InputManager::MouseB
|
|||
BrushComponentCode::mouse_button(drawP, genData, newMeshContainer->coords, button, relativeWidthResult.first.value());
|
||||
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newMeshContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
commit_data(false);
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited)
|
||||
|
|
|
|||
|
|
@ -267,7 +267,10 @@ void EditTool::edit_start(CanvasComponentContainer::ObjInfo* comp, bool initUndo
|
|||
}
|
||||
|
||||
bool EditTool::is_editable(CanvasComponentContainer::ObjInfo* comp) {
|
||||
return true; // Brush strokes used to not be editable, but now they are so this always returns true
|
||||
if(!comp || !comp->obj)
|
||||
return false;
|
||||
// Host may edit fog; players may not (fog / player-locked layers).
|
||||
return drawP.layerMan.can_current_peer_edit_layer(comp->obj->parentLayer);
|
||||
}
|
||||
|
||||
void EditTool::tool_update() {
|
||||
|
|
|
|||
|
|
@ -88,12 +88,39 @@ void ImageEditTool::edit_gui(Toolbar& t) {
|
|||
}, resourceData->name, true);
|
||||
}
|
||||
});
|
||||
text_button(gui, "file freistellen", "Freistellen", {
|
||||
.wide = true,
|
||||
.onClick = [&] {
|
||||
drawP.process_images_on({comp}, true, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
text_label_centered(gui, "Loading resource...");
|
||||
});
|
||||
}
|
||||
|
||||
void ImageEditTool::right_click_popup_gui(Toolbar& t, Vector2f popupPos) {
|
||||
using namespace GUIStuff;
|
||||
using namespace ElementHelpers;
|
||||
|
||||
auto& gui = drawP.world.main.g.gui;
|
||||
drawP.right_click_action_menu(popupPos, [&] {
|
||||
text_label_light(gui, "Image");
|
||||
drawP.popup_menu_action_button("Freistellen", "Freistellen", [&] {
|
||||
drawP.process_images_on({comp}, true, true);
|
||||
});
|
||||
drawP.popup_menu_action_button("Paste Image", "Paste Image", [&, popupPos] {
|
||||
drawP.selection.deselect_all();
|
||||
drawP.request_clipboard_image_paste(popupPos * drawP.world.main.g.final_gui_scale(), false);
|
||||
});
|
||||
drawP.popup_menu_action_button("Paste Image Freistellen", "Paste Image + Freistellen", [&, popupPos] {
|
||||
drawP.selection.deselect_all();
|
||||
drawP.request_clipboard_image_paste(popupPos * drawP.world.main.g.final_gui_scale(), true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ImageEditTool::gui_phone_toolbox(PhoneDrawingProgramScreen& t) {
|
||||
using namespace GUIStuff;
|
||||
using namespace ElementHelpers;
|
||||
|
|
|
|||
|
|
@ -30,4 +30,5 @@ class ImageEditTool : public DrawingProgramEditToolBase {
|
|||
virtual void edit_update() override;
|
||||
virtual void edit_gui(Toolbar& t) override;
|
||||
virtual void gui_phone_toolbox(PhoneDrawingProgramScreen& t) override;
|
||||
virtual void right_click_popup_gui(Toolbar& t, Vector2f popupPos) override;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ void EllipseDrawTool::input_mouse_button_on_canvas_callback(const InputManager::
|
|||
newContainer->coords = drawP.world.drawData.cam.c;
|
||||
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited)
|
||||
commit();
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ void LineDrawTool::input_mouse_button_on_canvas_callback(const InputManager::Mou
|
|||
p.pos = ensure_points_have_distance(p.pos, p.pos, 1.0f);
|
||||
brushPoints.emplace_back(p);
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newMeshContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
commit_data(false);
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited)
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ void RectDrawTool::input_mouse_button_on_canvas_callback(const InputManager::Mou
|
|||
newRectangle.d.fillStrokeMode = static_cast<uint8_t>(fillStrokeMode);
|
||||
newContainer->coords = drawP.world.drawData.cam.c;
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited)
|
||||
commit();
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ void TextBoxTool::input_mouse_button_on_canvas_callback(const InputManager::Mous
|
|||
newContainer->coords = drawP.world.drawData.cam.c;
|
||||
newContainer->coords.scale_about_double(drawP.world.drawData.cam.c.from_space(startAt), 1.0 / drawP.world.main.get_scale_and_density_factor_gui());
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited) {
|
||||
make_sure_textbox_is_big();
|
||||
|
|
|
|||
|
|
@ -154,10 +154,29 @@ void GlobalConfig::set_config_json(InputManager& input, const nlohmann::json& j,
|
|||
try{j.at("flipZoomToolDirection").get_to(flipZoomToolDirection);} catch(...) {}
|
||||
try{j.at("realTimeEraser").get_to(realTimeEraser);} catch(...) {}
|
||||
try{j.at("imageCompressOnInsert").get_to(imageCompressOnInsert);} catch(...) {}
|
||||
try{j.at("imageCompressFormat").get_to(imageCompressFormat);} catch(...) {}
|
||||
try {
|
||||
const auto& v = j.at("imageCompressFormat");
|
||||
if(v.is_string())
|
||||
v.get_to(imageCompressFormat);
|
||||
else if(v.is_number_integer()) {
|
||||
// Legacy: enums were saved as ints before ADL serialize lived in ImagePreprocess.
|
||||
int n = v.get<int>();
|
||||
if(n >= 0 && n <= 2)
|
||||
imageCompressFormat = static_cast<ImagePreprocess::CompressFormat>(n);
|
||||
}
|
||||
} catch(...) {}
|
||||
try{j.at("imageWebpQuality").get_to(imageWebpQuality);} catch(...) {}
|
||||
try{j.at("imageFreistellenOnInsert").get_to(imageFreistellenOnInsert);} catch(...) {}
|
||||
try{j.at("imageFreistellenMode").get_to(imageFreistellenMode);} catch(...) {}
|
||||
try {
|
||||
const auto& v = j.at("imageFreistellenMode");
|
||||
if(v.is_string())
|
||||
v.get_to(imageFreistellenMode);
|
||||
else if(v.is_number_integer()) {
|
||||
int n = v.get<int>();
|
||||
if(n >= 0 && n <= 3)
|
||||
imageFreistellenMode = static_cast<ImagePreprocess::FreistellenMode>(n);
|
||||
}
|
||||
} catch(...) {}
|
||||
try{j.at("imageWhiteKeyThreshold").get_to(imageWhiteKeyThreshold);} catch(...) {}
|
||||
#ifndef __EMSCRIPTEN__
|
||||
try{j.at("checkForUpdates").get_to(checkForUpdates);} catch(...) {}
|
||||
|
|
|
|||
|
|
@ -144,16 +144,3 @@ NLOHMANN_JSON_SERIALIZE_ENUM(GlobalConfig::AntiAliasing, {
|
|||
{GlobalConfig::AntiAliasing::SKIA, "Skia"},
|
||||
{GlobalConfig::AntiAliasing::DYNAMIC_MSAA, "Dynamic MSAA"},
|
||||
})
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(ImagePreprocess::CompressFormat, {
|
||||
{ImagePreprocess::CompressFormat::KEEP, "Keep"},
|
||||
{ImagePreprocess::CompressFormat::WEBP, "WebP"},
|
||||
{ImagePreprocess::CompressFormat::PNG, "PNG"},
|
||||
})
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(ImagePreprocess::FreistellenMode, {
|
||||
{ImagePreprocess::FreistellenMode::OFF, "Off"},
|
||||
{ImagePreprocess::FreistellenMode::WHITE_KEY, "WhiteKey"},
|
||||
{ImagePreprocess::FreistellenMode::REMBG_SCHNELL, "RembgSchnell"},
|
||||
{ImagePreprocess::FreistellenMode::REMBG_ALLGEMEIN, "RembgAllgemein"},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
|
@ -35,6 +36,13 @@
|
|||
#ifndef __EMSCRIPTEN__
|
||||
#include <cstdlib>
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace ImagePreprocess {
|
||||
|
|
@ -130,47 +138,182 @@ std::optional<std::string> encode_bitmap(const SkBitmap& bitmap, CompressFormat
|
|||
}
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
std::optional<std::string> find_rembg_bin() {
|
||||
const char* envBin = std::getenv("TOWER_REMBG_BIN");
|
||||
if(envBin && envBin[0] && std::filesystem::is_regular_file(envBin))
|
||||
return std::string(envBin);
|
||||
const char* home = std::getenv("HOME");
|
||||
if(home) {
|
||||
std::filesystem::path venv = std::filesystem::path(home) / ".local/share/tower-rembg/bin/rembg";
|
||||
if(std::filesystem::is_regular_file(venv))
|
||||
return venv.string();
|
||||
std::filesystem::path local = std::filesystem::path(home) / ".local/bin/rembg";
|
||||
if(std::filesystem::is_regular_file(local))
|
||||
return local.string();
|
||||
std::filesystem::path exe_parent_dir() {
|
||||
#ifdef _WIN32
|
||||
constexpr DWORD STEP = 2048;
|
||||
std::string pathStr;
|
||||
DWORD copied = 0;
|
||||
do {
|
||||
pathStr.resize(pathStr.size() + STEP);
|
||||
copied = GetModuleFileNameA(nullptr, pathStr.data(), static_cast<DWORD>(pathStr.size()));
|
||||
} while(copied >= pathStr.size());
|
||||
pathStr.resize(copied);
|
||||
return std::filesystem::path(pathStr).parent_path();
|
||||
#elif defined(__linux__)
|
||||
std::error_code ec;
|
||||
auto p = std::filesystem::canonical("/proc/self/exe", ec);
|
||||
if(ec)
|
||||
return std::filesystem::current_path();
|
||||
return p.parent_path();
|
||||
#else
|
||||
return std::filesystem::current_path();
|
||||
#endif
|
||||
}
|
||||
// PATH lookup via `command -v`
|
||||
|
||||
std::filesystem::path user_home_dir() {
|
||||
if(const char* h = std::getenv("HOME"); h && h[0])
|
||||
return h;
|
||||
#ifdef _WIN32
|
||||
if(const char* u = std::getenv("USERPROFILE"); u && u[0])
|
||||
return u;
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
bool is_runnable_file(const std::filesystem::path& p) {
|
||||
std::error_code ec;
|
||||
return std::filesystem::is_regular_file(p, ec);
|
||||
}
|
||||
|
||||
std::optional<std::string> first_existing(std::initializer_list<std::filesystem::path> cands) {
|
||||
for(const auto& p : cands) {
|
||||
if(!p.empty() && is_runnable_file(p))
|
||||
return p.string();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> find_rembg_bin() {
|
||||
if(const char* envBin = std::getenv("TOWER_REMBG_BIN"); envBin && envBin[0] && is_runnable_file(envBin))
|
||||
return std::string(envBin);
|
||||
|
||||
auto exe = exe_parent_dir();
|
||||
auto home = user_home_dir();
|
||||
#ifdef _WIN32
|
||||
if(auto f = first_existing({
|
||||
exe / "rembg" / "Scripts" / "rembg.exe",
|
||||
exe / "rembg" / "bin" / "rembg.exe",
|
||||
exe / "rembg" / "rembg.exe",
|
||||
exe / "data" / "rembg" / "Scripts" / "rembg.exe",
|
||||
home / "AppData" / "Local" / "tower-rembg" / "Scripts" / "rembg.exe",
|
||||
home / ".local" / "share" / "tower-rembg" / "Scripts" / "rembg.exe",
|
||||
home / ".local" / "bin" / "rembg.exe"
|
||||
}))
|
||||
return f;
|
||||
FILE* pipe = _popen("where rembg 2>NUL", "r");
|
||||
#else
|
||||
if(auto f = first_existing({
|
||||
exe / "rembg" / "bin" / "rembg",
|
||||
exe / "rembg" / "rembg",
|
||||
exe / "data" / "rembg" / "bin" / "rembg",
|
||||
home / ".local" / "share" / "infinipaint" / "rembg" / "bin" / "rembg",
|
||||
home / ".local" / "share" / "tower-rembg" / "bin" / "rembg",
|
||||
home / ".local" / "bin" / "rembg"
|
||||
}))
|
||||
return f;
|
||||
FILE* pipe = popen("command -v rembg 2>/dev/null", "r");
|
||||
#endif
|
||||
if(!pipe)
|
||||
return std::nullopt;
|
||||
char buf[512];
|
||||
std::string path;
|
||||
while(fgets(buf, sizeof(buf), pipe))
|
||||
path += buf;
|
||||
#ifdef _WIN32
|
||||
_pclose(pipe);
|
||||
#else
|
||||
pclose(pipe);
|
||||
#endif
|
||||
while(!path.empty() && (path.back() == '\n' || path.back() == '\r'))
|
||||
path.pop_back();
|
||||
if(!path.empty() && std::filesystem::is_regular_file(path))
|
||||
// `where` can return multiple lines; take the first
|
||||
auto nl = path.find('\n');
|
||||
if(nl != std::string::npos)
|
||||
path.resize(nl);
|
||||
if(!path.empty() && is_runnable_file(path))
|
||||
return path;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::filesystem::path rembg_home_dir() {
|
||||
if(const char* e = std::getenv("REMBG_HOME"); e && e[0])
|
||||
return e;
|
||||
if(const char* u = std::getenv("U2NET_HOME"); u && u[0])
|
||||
return u;
|
||||
auto exe = exe_parent_dir();
|
||||
std::error_code ec;
|
||||
auto bundled = exe / "rembg-home";
|
||||
if(std::filesystem::is_directory(bundled / "models", ec))
|
||||
return bundled;
|
||||
auto nextToRembg = exe / "rembg" / "models";
|
||||
if(std::filesystem::is_directory(nextToRembg, ec))
|
||||
return exe / "rembg";
|
||||
auto app = user_home_dir() / ".local" / "share" / "infinipaint" / "rembg-home";
|
||||
if(std::filesystem::is_directory(app / "models", ec))
|
||||
return app;
|
||||
auto home = user_home_dir();
|
||||
if(!home.empty())
|
||||
return home / ".rembg";
|
||||
return {};
|
||||
}
|
||||
|
||||
bool model_file_exists(std::string_view model) {
|
||||
std::string name(model);
|
||||
std::error_code ec;
|
||||
auto home = rembg_home_dir();
|
||||
std::vector<std::filesystem::path> cands;
|
||||
if(!home.empty()) {
|
||||
cands.push_back(home / "models" / name / (name + ".onnx"));
|
||||
cands.push_back(home / (name + ".onnx"));
|
||||
}
|
||||
auto xdg = std::getenv("XDG_DATA_HOME");
|
||||
if(xdg && xdg[0])
|
||||
cands.push_back(std::filesystem::path(xdg) / ".u2net" / (name + ".onnx"));
|
||||
auto uh = user_home_dir();
|
||||
if(!uh.empty()) {
|
||||
cands.push_back(uh / ".u2net" / (name + ".onnx"));
|
||||
cands.push_back(uh / ".rembg" / "models" / name / (name + ".onnx"));
|
||||
}
|
||||
for(const auto& p : cands) {
|
||||
if(std::filesystem::is_regular_file(p, ec) && std::filesystem::file_size(p, ec) > 1024)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct RembgCache {
|
||||
std::optional<std::string> bin;
|
||||
bool silueta = false;
|
||||
bool bria = false;
|
||||
};
|
||||
|
||||
RembgCache& rembg_cache() {
|
||||
// Rescan each call — CLI/models may appear after install-local without restart.
|
||||
static RembgCache c;
|
||||
c.bin = find_rembg_bin();
|
||||
c.silueta = model_file_exists("silueta");
|
||||
c.bria = model_file_exists("bria-rmbg");
|
||||
return c;
|
||||
}
|
||||
|
||||
std::optional<std::string> run_rembg(const std::string& inputData, const std::string& model) {
|
||||
auto rembg = find_rembg_bin();
|
||||
auto rembg = rembg_cache().bin;
|
||||
if(!rembg) {
|
||||
Logger::get().log(Logger::LogType::INFO, "[ImagePreprocess] rembg not found — falling back to white-key");
|
||||
Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "[Freistellen] rembg CLI not found");
|
||||
return std::nullopt;
|
||||
}
|
||||
if((model == "silueta" && !rembg_cache().silueta) || (model == "bria-rmbg" && !rembg_cache().bria)) {
|
||||
Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "[Freistellen] rembg model missing: " + model);
|
||||
return std::nullopt;
|
||||
}
|
||||
std::error_code ec;
|
||||
auto tmpDir = std::filesystem::temp_directory_path(ec);
|
||||
if(ec)
|
||||
return std::nullopt;
|
||||
auto inPath = tmpDir / ("infinipaint-rembg-in-" + std::to_string(std::random_device{}()) + ".png");
|
||||
auto outPath = tmpDir / ("infinipaint-rembg-out-" + std::to_string(std::random_device{}()) + ".png");
|
||||
auto tag = std::to_string(std::random_device{}());
|
||||
auto inPath = tmpDir / ("infinipaint-rembg-in-" + tag + ".png");
|
||||
auto outPath = tmpDir / ("infinipaint-rembg-out-" + tag + ".png");
|
||||
auto errPath = tmpDir / ("infinipaint-rembg-err-" + tag + ".txt");
|
||||
|
||||
{
|
||||
std::ofstream out(inPath, std::ios::binary);
|
||||
|
|
@ -179,7 +322,19 @@ std::optional<std::string> run_rembg(const std::string& inputData, const std::st
|
|||
out.write(inputData.data(), static_cast<std::streamsize>(inputData.size()));
|
||||
}
|
||||
|
||||
std::string cmd = "\"" + *rembg + "\" i -m " + model + " \"" + inPath.string() + "\" \"" + outPath.string() + "\" 2>/dev/null";
|
||||
auto home = rembg_home_dir();
|
||||
// Match Tower freistellen inbox: post-process mask + decontaminate fringing.
|
||||
#ifdef _WIN32
|
||||
std::string envPrefix;
|
||||
if(!home.empty())
|
||||
envPrefix = "set \"REMBG_HOME=" + home.string() + "\" && set \"U2NET_HOME=" + home.string() + "\" && ";
|
||||
std::string cmd = envPrefix + "\"" + *rembg + "\" i -m " + model + " -ppm -dc \"" + inPath.string() + "\" \"" + outPath.string() + "\" 2>\"" + errPath.string() + "\"";
|
||||
#else
|
||||
std::string cmd = "\"" + *rembg + "\" i -m " + model + " -ppm -dc \"" + inPath.string() + "\" \"" + outPath.string() + "\" 2>\"" + errPath.string() + "\"";
|
||||
if(!home.empty())
|
||||
cmd = "REMBG_HOME=\"" + home.string() + "\" U2NET_HOME=\"" + home.string() + "\" " + cmd;
|
||||
#endif
|
||||
Logger::get().log(Logger::LogType::INFO, "[Freistellen] running rembg -m " + model + " -ppm -dc");
|
||||
int rc = std::system(cmd.c_str());
|
||||
std::optional<std::string> result;
|
||||
if(rc == 0 && std::filesystem::is_regular_file(outPath)) {
|
||||
|
|
@ -190,8 +345,21 @@ std::optional<std::string> run_rembg(const std::string& inputData, const std::st
|
|||
result.reset();
|
||||
}
|
||||
}
|
||||
if(!result) {
|
||||
std::string err;
|
||||
{
|
||||
std::ifstream e(errPath);
|
||||
if(e)
|
||||
err = std::string(std::istreambuf_iterator<char>(e), std::istreambuf_iterator<char>());
|
||||
}
|
||||
if(err.size() > 400)
|
||||
err.resize(400);
|
||||
Logger::get().log(Logger::LogType::DESKTOP_USERINFO,
|
||||
"[Freistellen] rembg failed (rc=" + std::to_string(rc) + ")" + (err.empty() ? "" : ": " + err));
|
||||
}
|
||||
std::filesystem::remove(inPath, ec);
|
||||
std::filesystem::remove(outPath, ec);
|
||||
std::filesystem::remove(errPath, ec);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -227,12 +395,24 @@ Options options_from_config(const GlobalConfig& conf) {
|
|||
o.compressOnInsert = conf.imageCompressOnInsert;
|
||||
o.compressFormat = conf.imageCompressFormat;
|
||||
o.webpQuality = conf.imageWebpQuality;
|
||||
o.freistellenOnInsert = conf.imageFreistellenOnInsert;
|
||||
o.freistellenOnInsert = false;
|
||||
o.freistellenMode = conf.imageFreistellenMode;
|
||||
#ifndef __EMSCRIPTEN__
|
||||
if(!freistellen_mode_available(o.freistellenMode))
|
||||
o.freistellenMode = FreistellenMode::WHITE_KEY;
|
||||
#endif
|
||||
o.whiteKeyThreshold = conf.imageWhiteKeyThreshold;
|
||||
return o;
|
||||
}
|
||||
|
||||
Options options_for_place(const GlobalConfig& conf, bool applyFreistellen) {
|
||||
Options o = options_from_config(conf);
|
||||
o.freistellenOnInsert = applyFreistellen;
|
||||
if(applyFreistellen && o.freistellenMode == FreistellenMode::OFF)
|
||||
o.freistellenMode = FreistellenMode::WHITE_KEY;
|
||||
return o;
|
||||
}
|
||||
|
||||
bool preprocess_resource(std::string& name, std::shared_ptr<std::string>& data, const Options& options) {
|
||||
if(!data || data->empty())
|
||||
return false;
|
||||
|
|
@ -264,17 +444,7 @@ bool preprocess_resource(std::string& name, std::shared_ptr<std::string>& data,
|
|||
changed = true;
|
||||
name = replace_extension(name, ".png");
|
||||
}
|
||||
else {
|
||||
// Fallback white-key
|
||||
if(decode_first_frame(working, bmp)) {
|
||||
apply_white_key(bmp, options.whiteKeyThreshold);
|
||||
if(auto enc = encode_bitmap(bmp, CompressFormat::PNG, 100)) {
|
||||
working = *enc;
|
||||
changed = true;
|
||||
name = replace_extension(name, ".png");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not fall back to white-key — that looked "instant" and much worse than Tower.
|
||||
}
|
||||
else
|
||||
#endif
|
||||
|
|
@ -312,4 +482,74 @@ bool preprocess_resource(std::string& name, std::shared_ptr<std::string>& data,
|
|||
return changed;
|
||||
}
|
||||
|
||||
std::optional<CompressSizeEstimate> estimate_compress_size(std::string_view name, std::string_view data, CompressFormat format, int webpQuality) {
|
||||
if(data.empty() || !looks_like_raster_image(name, data))
|
||||
return std::nullopt;
|
||||
CompressSizeEstimate out;
|
||||
out.currentBytes = static_cast<uint64_t>(data.size());
|
||||
out.estimatedBytes = out.currentBytes;
|
||||
if(format == CompressFormat::KEEP)
|
||||
return out;
|
||||
SkBitmap bmp;
|
||||
std::string working(data);
|
||||
if(!decode_first_frame(working, bmp))
|
||||
return out;
|
||||
auto enc = encode_bitmap(bmp, format, webpQuality);
|
||||
if(!enc)
|
||||
return out;
|
||||
bool preferNew = enc->size() < working.size() || format == CompressFormat::WEBP;
|
||||
if(preferNew)
|
||||
out.estimatedBytes = static_cast<uint64_t>(enc->size());
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string format_byte_size(uint64_t bytes) {
|
||||
const char* units[] = {"B", "KB", "MB", "GB", "TB"};
|
||||
double v = static_cast<double>(bytes);
|
||||
int u = 0;
|
||||
while(v >= 1024.0 && u < 4) {
|
||||
v /= 1024.0;
|
||||
u++;
|
||||
}
|
||||
char buf[64];
|
||||
if(u == 0)
|
||||
std::snprintf(buf, sizeof(buf), "%llu B", static_cast<unsigned long long>(bytes));
|
||||
else
|
||||
std::snprintf(buf, sizeof(buf), "%.1f %s", v, units[u]);
|
||||
return buf;
|
||||
}
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
bool rembg_cli_available() {
|
||||
return rembg_cache().bin.has_value();
|
||||
}
|
||||
|
||||
bool rembg_model_available(std::string_view model) {
|
||||
auto& c = rembg_cache();
|
||||
if(model == "silueta")
|
||||
return c.silueta;
|
||||
if(model == "bria-rmbg")
|
||||
return c.bria;
|
||||
return model_file_exists(model);
|
||||
}
|
||||
|
||||
bool freistellen_mode_available(FreistellenMode mode) {
|
||||
switch(mode) {
|
||||
case FreistellenMode::OFF:
|
||||
case FreistellenMode::WHITE_KEY:
|
||||
return true;
|
||||
case FreistellenMode::REMBG_SCHNELL:
|
||||
return rembg_cli_available() && rembg_model_available("silueta");
|
||||
case FreistellenMode::REMBG_ALLGEMEIN:
|
||||
return rembg_cli_available() && rembg_model_available("bria-rmbg");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string rembg_cli_path_string() {
|
||||
auto& c = rembg_cache();
|
||||
return c.bin ? *c.bin : std::string{};
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
class GlobalConfig;
|
||||
|
||||
|
|
@ -26,6 +28,20 @@ enum class FreistellenMode : uint8_t {
|
|||
REMBG_ALLGEMEIN // rembg -m bria-rmbg (Tower freistellen/allgemein)
|
||||
};
|
||||
|
||||
// Must live in this namespace so nlohmann ADL serializes strings (not raw ints).
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(CompressFormat, {
|
||||
{CompressFormat::KEEP, "Keep"},
|
||||
{CompressFormat::WEBP, "WebP"},
|
||||
{CompressFormat::PNG, "PNG"},
|
||||
})
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(FreistellenMode, {
|
||||
{FreistellenMode::OFF, "Off"},
|
||||
{FreistellenMode::WHITE_KEY, "WhiteKey"},
|
||||
{FreistellenMode::REMBG_SCHNELL, "RembgSchnell"},
|
||||
{FreistellenMode::REMBG_ALLGEMEIN, "RembgAllgemein"},
|
||||
})
|
||||
|
||||
struct Options {
|
||||
bool compressOnInsert = true;
|
||||
CompressFormat compressFormat = CompressFormat::WEBP;
|
||||
|
|
@ -39,7 +55,29 @@ struct Options {
|
|||
bool preprocess_resource(std::string& name, std::shared_ptr<std::string>& data, const Options& options);
|
||||
|
||||
Options options_from_config(const GlobalConfig& conf);
|
||||
/** Compress from settings; freistellen only if applyFreistellen (never from the old on-insert checkbox). */
|
||||
Options options_for_place(const GlobalConfig& conf, bool applyFreistellen);
|
||||
|
||||
/** Dry-run encode only (no freistellen, no mutation). Nullopt if not a raster. KEEP is treated as no-op (estimated == current). */
|
||||
struct CompressSizeEstimate {
|
||||
uint64_t currentBytes = 0;
|
||||
uint64_t estimatedBytes = 0;
|
||||
};
|
||||
std::optional<CompressSizeEstimate> estimate_compress_size(std::string_view name, std::string_view data, CompressFormat format, int webpQuality);
|
||||
|
||||
std::string format_byte_size(uint64_t bytes);
|
||||
|
||||
bool looks_like_raster_image(std::string_view name, std::string_view data);
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
/** rembg CLI found (bundled next to app, Tower venv, PATH, or TOWER_REMBG_BIN). */
|
||||
bool rembg_cli_available();
|
||||
/** Model files present under REMBG_HOME / next to bundled rembg / ~/.rembg. model: "silueta" or "bria-rmbg". */
|
||||
bool rembg_model_available(std::string_view model);
|
||||
/** CLI + model for this freistellen mode (WHITE_KEY/OFF always true). */
|
||||
bool freistellen_mode_available(FreistellenMode mode);
|
||||
/** Short path for UI status, or empty. */
|
||||
std::string rembg_cli_path_string();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -804,14 +804,16 @@ void InputManager::call_paste(CustomEvents::PasteEvent::DataType type, const Inp
|
|||
case CustomEvents::PasteEvent::DataType::IMAGE: {
|
||||
struct PasteData {
|
||||
std::optional<Vector2f> pos;
|
||||
bool applyFreistellen;
|
||||
InputManager* t;
|
||||
};
|
||||
static PasteData pasteData;
|
||||
pasteData.pos = info.pastePosition;
|
||||
pasteData.applyFreistellen = info.applyFreistellen;
|
||||
pasteData.t = this;
|
||||
emscripten_browser_clipboard::paste_async_image([](std::string_view pasteData, void* callbackData){
|
||||
PasteData* p = (PasteData*)callbackData;
|
||||
p->t->process_image_paste(pasteData, p->pos);
|
||||
p->t->process_image_paste(pasteData, p->pos, p->applyFreistellen);
|
||||
}, &pasteData);
|
||||
break;
|
||||
}
|
||||
|
|
@ -823,7 +825,7 @@ void InputManager::call_paste(CustomEvents::PasteEvent::DataType type, const Inp
|
|||
break;
|
||||
case CustomEvents::PasteEvent::DataType::IMAGE:
|
||||
get_clipboard_image_data_SDL([&](std::string_view pasteData) {
|
||||
process_image_paste(pasteData, info.pastePosition);
|
||||
process_image_paste(pasteData, info.pastePosition, info.applyFreistellen);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
@ -838,11 +840,12 @@ void InputManager::process_text_paste(const std::string& pasteStr, bool allowRic
|
|||
});
|
||||
}
|
||||
|
||||
void InputManager::process_image_paste(std::string_view pasteData, const std::optional<Vector2f>& pastePos) {
|
||||
void InputManager::process_image_paste(std::string_view pasteData, const std::optional<Vector2f>& pastePos, bool applyFreistellen) {
|
||||
CustomEvents::emit_event<CustomEvents::PasteEvent>({
|
||||
.type = CustomEvents::PasteEvent::DataType::IMAGE,
|
||||
.data = std::string(pasteData),
|
||||
.mousePos = pastePos
|
||||
.mousePos = pastePos,
|
||||
.applyFreistellen = applyFreistellen
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class MainProgram;
|
|||
struct InputManagerCallPasteInfo {
|
||||
std::optional<Vector2f> pastePosition;
|
||||
bool allowRichText = true;
|
||||
bool applyFreistellen = false;
|
||||
};
|
||||
|
||||
struct InputManager {
|
||||
|
|
@ -286,7 +287,7 @@ struct InputManager {
|
|||
|
||||
void call_paste(CustomEvents::PasteEvent::DataType type, const InputManagerCallPasteInfo& info = {});
|
||||
void process_text_paste(const std::string& pasteStr, bool allowRichText);
|
||||
void process_image_paste(std::string_view pasteData, const std::optional<Vector2f>& pastePos);
|
||||
void process_image_paste(std::string_view pasteData, const std::optional<Vector2f>& pastePos, bool applyFreistellen = false);
|
||||
|
||||
std::optional<RichText::TextData> lastCopiedRichText;
|
||||
|
||||
|
|
|
|||
|
|
@ -1544,21 +1544,78 @@ void Toolbar::general_settings_inner_gui() {
|
|||
{"PNG", ImagePreprocess::CompressFormat::PNG}
|
||||
});
|
||||
slider_scalar_field(gui, "webp quality", "WebP quality", &main.conf.imageWebpQuality, 1, 100);
|
||||
checkbox_boolean_field(gui, "freistellen on insert", "Freistellen / remove background on insert", &main.conf.imageFreistellenOnInsert);
|
||||
text_label(gui, "Freistellen mode:");
|
||||
if(main.world) {
|
||||
auto sizes = main.world->drawProg.canvas_image_size_summary();
|
||||
mutable_text_label_light(gui, "canvas image size now", sizes.nowLine);
|
||||
if(!sizes.afterLine.empty())
|
||||
mutable_text_label_light(gui, "canvas image size after", sizes.afterLine);
|
||||
text_label_light(gui, "Unique raster ResourceData bytes (shared copies counted once). .infpnt is also zstd, so the save file is usually smaller than this plus drawing data.");
|
||||
}
|
||||
text_label(gui, "Freistellen (on demand):");
|
||||
text_label_light(gui, "Not applied on import. Choose when pasting (Ctrl+Shift+V or right-click → Paste Image + Freistellen), or right-click an image.");
|
||||
#ifndef __EMSCRIPTEN__
|
||||
{
|
||||
using FM = ImagePreprocess::FreistellenMode;
|
||||
const char* modeLabel = "Off";
|
||||
switch(main.conf.imageFreistellenMode) {
|
||||
case FM::OFF: modeLabel = "Off (Freistellen falls back to white-key)"; break;
|
||||
case FM::WHITE_KEY: modeLabel = "White background"; break;
|
||||
case FM::REMBG_SCHNELL: modeLabel = "Schnell (silueta)"; break;
|
||||
case FM::REMBG_ALLGEMEIN: modeLabel = "Allgemein (bria-rmbg)"; break;
|
||||
}
|
||||
text_label_light(gui, std::string("Active: ") + modeLabel);
|
||||
}
|
||||
auto rembg_mode_row = [&](const char* id, std::string_view label, ImagePreprocess::FreistellenMode mode, bool available) {
|
||||
gui.new_id(id, [&] {
|
||||
if(available) {
|
||||
radio_button_field(gui, "Option", label,
|
||||
[&]{ return main.conf.imageFreistellenMode == mode; },
|
||||
[&]{
|
||||
main.conf.imageFreistellenMode = mode;
|
||||
main.save_config();
|
||||
});
|
||||
}
|
||||
else {
|
||||
text_label_light(gui, std::string(label) + " (unavailable — rembg/model not installed)");
|
||||
}
|
||||
});
|
||||
};
|
||||
gui.new_id("freistellen mode", [&] {
|
||||
rembg_mode_row("off", "Off", ImagePreprocess::FreistellenMode::OFF, true);
|
||||
rembg_mode_row("white", "White background", ImagePreprocess::FreistellenMode::WHITE_KEY, true);
|
||||
rembg_mode_row("schnell", "Schnell (rembg silueta)", ImagePreprocess::FreistellenMode::REMBG_SCHNELL,
|
||||
ImagePreprocess::freistellen_mode_available(ImagePreprocess::FreistellenMode::REMBG_SCHNELL));
|
||||
rembg_mode_row("allgemein", "Allgemein (rembg bria)", ImagePreprocess::FreistellenMode::REMBG_ALLGEMEIN,
|
||||
ImagePreprocess::freistellen_mode_available(ImagePreprocess::FreistellenMode::REMBG_ALLGEMEIN));
|
||||
});
|
||||
if(ImagePreprocess::rembg_cli_available()) {
|
||||
text_label_light(gui, "rembg: " + ImagePreprocess::rembg_cli_path_string());
|
||||
}
|
||||
else {
|
||||
text_label_light(gui, "rembg not found. White-key still works. To enable Schnell/Allgemein, put a rembg venv in rembg/ next to the binary (models in rembg-home/models/) or install Tower rembg.");
|
||||
}
|
||||
#else
|
||||
radio_button_selector(gui, "freistellen mode", &main.conf.imageFreistellenMode, {
|
||||
{"Off", ImagePreprocess::FreistellenMode::OFF},
|
||||
{"White background", ImagePreprocess::FreistellenMode::WHITE_KEY},
|
||||
#ifndef __EMSCRIPTEN__
|
||||
{"Schnell (rembg silueta)", ImagePreprocess::FreistellenMode::REMBG_SCHNELL},
|
||||
{"Allgemein (rembg bria)", ImagePreprocess::FreistellenMode::REMBG_ALLGEMEIN},
|
||||
#endif
|
||||
});
|
||||
slider_scalar_field(gui, "white key threshold", "White-key threshold", &main.conf.imageWhiteKeyThreshold, 200, 255);
|
||||
text_label_light(gui, "Selection toolbar: Compress Images / Freistellen also work afterwards.");
|
||||
#ifndef __EMSCRIPTEN__
|
||||
text_label_light(gui, "rembg uses Tower venv (~/.local/share/tower-rembg) when present.");
|
||||
#endif
|
||||
slider_scalar_field(gui, "white key threshold", "White-key threshold", &main.conf.imageWhiteKeyThreshold, 200, 255);
|
||||
text_label(gui, "Already on canvas:");
|
||||
text_button(gui, "compress all canvas images", "Compress all images on canvas", {
|
||||
.onClick = [&] {
|
||||
if(main.world)
|
||||
main.world->drawProg.process_all_canvas_images(false, true);
|
||||
}
|
||||
});
|
||||
text_button(gui, "freistellen all canvas images", "Freistellen all images on canvas", {
|
||||
.onClick = [&] {
|
||||
if(main.world)
|
||||
main.world->drawProg.process_all_canvas_images(true, true);
|
||||
}
|
||||
});
|
||||
text_label_light(gui, "Uses the format and quality above (Keep → WebP). Shared copies of the same file are compressed once. Can take a while on large canvases.");
|
||||
text_label_light(gui, "Or select images and use Compress Images / Freistellen in the selection panel, or right-click.");
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ VersionNumber header_to_version_number(const std::string& header) {
|
|||
m["INFPNT000005"] = VersionNumber(0, 4, 0);
|
||||
m["INFPNT000006"] = VersionNumber(0, 6, 0);
|
||||
m["INFPNT000007"] = VersionNumber(0, 7, 0);
|
||||
m["INFPNT000008"] = VersionNumber(0, 8, 0);
|
||||
}
|
||||
auto it = m.find(header);
|
||||
if(it == m.end())
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace VersionConstants {
|
|||
VersionNumber header_to_version_number(const std::string& header);
|
||||
|
||||
constexpr int SAVEFILE_HEADER_LEN = 12; // DO NOT CHANGE THIS HEADER LENGTH
|
||||
const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000007"; // Change whenever the save file is incompatible with the previous version
|
||||
const std::string CURRENT_VERSION_STRING = "0.7.0-fork";
|
||||
constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 7, 0);
|
||||
const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000008"; // Change whenever the save file is incompatible with the previous version
|
||||
const std::string CURRENT_VERSION_STRING = "0.8.0-fork";
|
||||
constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 8, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include <Helpers/NetworkingObjects/DelayUpdateSerializedClassManager.hpp>
|
||||
#include <cereal/types/unordered_map.hpp>
|
||||
#include "DrawingProgram/Layers/DrawingProgramLayerListItem.hpp"
|
||||
#include "DrawingProgram/Layers/FogStripper.hpp"
|
||||
#include "FingerInputTracker.hpp"
|
||||
#include "Helpers/NetworkingObjects/NetObjOrderedList.hpp"
|
||||
#include "Helpers/NetworkingObjects/NetObjTemporaryPtr.decl.hpp"
|
||||
|
|
@ -449,13 +450,19 @@ void World::start_hosting(const std::string& initNetSource, const std::string& s
|
|||
#endif
|
||||
}
|
||||
netServer->send_string_stream_to_client(client, RELIABLE_COMMAND_CHANNEL, ss);
|
||||
auto visibleResources = FogStripper::compute_visible_resource_ids(drawProg.layerMan, drawProg.layerMan.clientHiddenComponentIds);
|
||||
for(auto& r : rMan.resource_list()) {
|
||||
if(!visibleResources.contains(r.get_net_id()))
|
||||
continue;
|
||||
netServer->send_items_to_client(client, RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE_ID, r.get_net_id());
|
||||
netServer->send_items_to_client(client, RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE_DATA, *r);
|
||||
}
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_UPDATE_NETWORK_OBJECT, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
currentNetUpdateClient = client;
|
||||
netObjMan.read_update_message(message, client);
|
||||
currentNetUpdateClient = nullptr;
|
||||
drawProg.layerMan.flush_deferred_rejects();
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_KEEP_ALIVE, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
});
|
||||
|
|
|
|||
|
|
@ -112,6 +112,8 @@ class World {
|
|||
|
||||
std::shared_ptr<NetServer> netServer;
|
||||
std::shared_ptr<NetClient> netClient;
|
||||
// Non-null only while the host is inside a client-originated NetObj update handler.
|
||||
std::shared_ptr<NetServer::ClientData> currentNetUpdateClient;
|
||||
|
||||
void input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile);
|
||||
void input_paste_callback(const CustomEvents::PasteEvent& paste);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue