Ship 0.8 fog/freistellen and fix Windows CI for friends
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:
copilot 2026-10-25 18:07:47 +01:00
parent 97b136de3b
commit 1c1d63f376
46 changed files with 1748 additions and 156 deletions

View file

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

View file

@ -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,7 +424,11 @@ 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)

21
FORK.md
View file

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

View file

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

View file

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

View file

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

View 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++'}

View 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"]

View file

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

View file

@ -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&lt;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
View 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

View file

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

View file

@ -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();
}
});
}

View file

@ -65,6 +65,7 @@ namespace CustomEvents {
std::string data;
std::optional<RichText::TextData> richText;
std::optional<Vector2f> mousePos;
bool applyFreistellen = false;
};
struct OpenInfiniPaintFileEvent {

View file

@ -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,8 +540,12 @@ 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) {
@ -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() {

View file

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

View file

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

View file

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

View file

@ -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();

View file

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

View file

@ -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();
}
});

View file

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

View file

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

View file

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

View file

@ -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.");
}
}
}
}

View 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

View 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);
}

View file

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

View file

@ -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() {

View file

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

View file

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

View file

@ -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();

View file

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

View file

@ -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();

View file

@ -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();

View file

@ -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(...) {}

View file

@ -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"},
})

View file

@ -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
}
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();
}
// PATH lookup via `command -v`
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
}

View file

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

View file

@ -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
});
}

View file

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

View file

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

View file

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

View file

@ -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);
}

View file

@ -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) {
});

View file

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