diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index d7362dc..e3919d9 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -46,12 +46,12 @@ jobs: } - name: XML, desktop entry, AppStream, Flatpak manifest run: | - xmllint --noout packaging/windows/slidecraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* + xmllint --noout packaging/windows/deckcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* # Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon. - if grep -oE '"$RUNNER_TEMP/ai.storyteller.slidecraft.metainfo.xml" - appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.slidecraft.metainfo.xml" - python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.slidecraft.yml')); assert m['id'] == 'ai.storyteller.slidecraft' and m['command'] == 'slidecraft'" + desktop-file-validate packaging/linux/ai.storyteller.deckcraft.desktop + sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.deckcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.deckcraft.metainfo.xml" + appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.deckcraft.metainfo.xml" + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.deckcraft.yml')); assert m['id'] == 'ai.storyteller.deckcraft' and m['command'] == 'deckcraft'" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b660831..9c85b4f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ # Release pipeline: every push to `release` builds installers for every platform and creates or -# updates the draft GitHub Release "SlideCraft v" (tag v). Recipe: +# updates the draft GitHub Release "DeckCraft v" (tag v). Recipe: # craftrules release/playbook.md. # # The version has one source: `[workspace.package] version` in Cargo.toml (`cargo xtask version`, @@ -33,7 +33,7 @@ env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 - SLIDECRAFT_BUILD_SHA: ${{ github.sha }} + DECKCRAFT_BUILD_SHA: ${{ github.sha }} INPUT_VERSION: ${{ inputs.version }} # Optional font input (storytold/craft-fonts docs/integration.md); releases always embed it. # This commit adds fonts/latin-manifest.txt (the Latin presentation fonts). @@ -66,7 +66,7 @@ jobs: echo "date=$(date -u +%Y-%m-%d)" echo "prerelease=$pre" } >>"$GITHUB_OUTPUT" - echo "### SlideCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" + echo "### DeckCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" macos: name: macOS universal @@ -74,8 +74,8 @@ jobs: runs-on: macos-15 environment: release env: - SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} - SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + DECKCRAFT_VERSION: ${{ needs.version.outputs.version }} + DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 - name: Check out craft-fonts @@ -92,7 +92,7 @@ jobs: key: release-macos-universal - name: Apply version override if: inputs.version != '' - run: cargo xtask version set "$SLIDECRAFT_VERSION" + run: cargo xtask version set "$DECKCRAFT_VERSION" - name: Import signing certificate env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} @@ -129,8 +129,8 @@ jobs: - { arch: x64, target: x86_64-pc-windows-msvc } - { arch: x86, target: i686-pc-windows-msvc } env: - SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} - SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + DECKCRAFT_VERSION: ${{ needs.version.outputs.version }} + DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 - name: Check out craft-fonts @@ -148,7 +148,7 @@ jobs: - name: Apply version override if: inputs.version != '' shell: bash - run: cargo xtask version set "$SLIDECRAFT_VERSION" + run: cargo xtask version set "$DECKCRAFT_VERSION" - name: Install WiX shell: pwsh run: dotnet tool install --global wix --version 5.0.2 @@ -186,8 +186,8 @@ jobs: - { arch: x86_64, runner: ubuntu-22.04, deb: amd64 } - { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 } env: - SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} - SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + DECKCRAFT_VERSION: ${{ needs.version.outputs.version }} + DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} NFPM_VERSION: 2.47.0 steps: - uses: actions/checkout@v4 @@ -212,7 +212,7 @@ jobs: key: release-linux-${{ matrix.arch }} - name: Apply version override if: inputs.version != '' - run: cargo xtask version set "$SLIDECRAFT_VERSION" + run: cargo xtask version set "$DECKCRAFT_VERSION" - name: Build and package (AppImage, deb, rpm, tar.gz) env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts @@ -221,8 +221,8 @@ jobs: - name: Check the packages run: | dpkg-deb --info dist/release/*.deb - dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/slidecraft$|metainfo|applications' - ldd target/release/slidecraft + dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/deckcraft$|metainfo|applications' + ldd target/release/deckcraft for appimage in dist/release/*.AppImage; do chmod +x "$appimage" APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version @@ -244,14 +244,14 @@ jobs: image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08 options: --privileged env: - SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} + DECKCRAFT_VERSION: ${{ needs.version.outputs.version }} steps: - uses: actions/checkout@v4 - name: Apply version override if: inputs.version != '' run: | # No cargo in this container: the same edit `cargo xtask version set` makes. - sed -i "0,/^version = \".*\"/s//version = \"$SLIDECRAFT_VERSION\"/" Cargo.toml + sed -i "0,/^version = \".*\"/s//version = \"$DECKCRAFT_VERSION\"/" Cargo.toml grep -m1 '^version' Cargo.toml - name: Vendor the crate sources (flatpak builds run offline) run: | @@ -262,14 +262,14 @@ jobs: python3 "$RUNNER_TEMP/flatpak-cargo-generator.py" Cargo.lock -o packaging/linux/flatpak/cargo-sources.json - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: - manifest-path: packaging/linux/flatpak/ai.storyteller.slidecraft.yml - bundle: slidecraft-${{ needs.version.outputs.version }}-linux-x86_64.flatpak + manifest-path: packaging/linux/flatpak/ai.storyteller.deckcraft.yml + bundle: deckcraft-${{ needs.version.outputs.version }}-linux-x86_64.flatpak upload-artifact: false cache-key: flatpak-builder-${{ github.sha }} - uses: actions/upload-artifact@v4 with: name: release-flatpak - path: slidecraft-*.flatpak + path: deckcraft-*.flatpak if-no-files-found: error freebsd: @@ -279,8 +279,8 @@ jobs: environment: release timeout-minutes: 240 env: - SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} - SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + DECKCRAFT_VERSION: ${{ needs.version.outputs.version }} + DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} VERSION_OVERRIDE: ${{ inputs.version }} steps: - uses: actions/checkout@v4 @@ -297,7 +297,7 @@ jobs: usesh: true cpu: 4 mem: 12288 - envs: SLIDECRAFT_VERSION SLIDECRAFT_BUILD_DATE SLIDECRAFT_BUILD_SHA VERSION_OVERRIDE + envs: DECKCRAFT_VERSION DECKCRAFT_BUILD_DATE DECKCRAFT_BUILD_SHA VERSION_OVERRIDE # Copies the workspace (with dist/release) back; the target dir lives outside it so the # copy stays small. copyback: true @@ -308,7 +308,7 @@ jobs: . "$HOME/.cargo/env" git config --global --add safe.directory "*" export CARGO_TARGET_DIR="$HOME/target" CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1 - if [ -n "$VERSION_OVERRIDE" ]; then cargo xtask version set "$SLIDECRAFT_VERSION"; fi + if [ -n "$VERSION_OVERRIDE" ]; then cargo xtask version set "$DECKCRAFT_VERSION"; fi bash packaging/freebsd/package.sh - uses: actions/upload-artifact@v4 with: @@ -322,8 +322,8 @@ jobs: runs-on: ubuntu-latest environment: release env: - SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} - SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + DECKCRAFT_VERSION: ${{ needs.version.outputs.version }} + DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} TRUNK_VERSION: 0.21.14 steps: - uses: actions/checkout@v4 @@ -346,7 +346,7 @@ jobs: trunk --version - name: Apply version override if: inputs.version != '' - run: cargo xtask version set "$SLIDECRAFT_VERSION" + run: cargo xtask version set "$DECKCRAFT_VERSION" - name: Build and zip env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts @@ -389,7 +389,7 @@ jobs: working-directory: artifacts run: | tag="v$VERSION" - title="SlideCraft v$VERSION" + title="DeckCraft v$VERSION" pre=() if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then diff --git a/AGENTS.md b/AGENTS.md index 7bcdc20..0e706a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — rules for every agent and contributor -SlideCraft is a clean-room, open-source, pure-Rust presentation application targeting Microsoft +DeckCraft is a clean-room, open-source, pure-Rust presentation application targeting Microsoft PowerPoint parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and FreeBSD, and on the web via WASM. Siblings with the same conventions: PhotoCraft, VectorCraft, FilmCraft, LightCraft, PrintCraft, EffectCraft, DesignCraft @@ -32,7 +32,7 @@ the app renders from those. product" includes PowerPoint, Office, Windows and the fonts they install (Aptos, Calibri, Cambria, Segoe, Consolas, Wingdings…). - This holds even when the vendor publishes the material under an open licence, if it is **visual - design**: SlideCraft does not embed or reference Adobe's Source Sans/Serif/Han families or Noto + design**: DeckCraft does not embed or reference Adobe's Source Sans/Serif/Han families or Noto CJK (Source Han rebranded); `crates/fonts/build.rs` excludes them. - **Never** recreate, trace, redraw or closely imitate a vendor icon, theme or graphic. Our icons are drawn in code or come from openly licensed sets (Lucide, ISC), and our themes, colours, @@ -58,7 +58,7 @@ Forbidden: system fonts that are not openly licensed, vendor emoji artwork, stoc for personal use" material, screenshots of other people's software, anything whose licence you cannot prove from its original source. -**Screenshots of SlideCraft** are contributor-original. Everything visible in them must itself be +**Screenshots of DeckCraft** are contributor-original. Everything visible in them must itself be allowed: our UI, our/Lucide icons, OFL fonts, and decks built only from allowed assets. ### 1.3 Every asset is attributed @@ -69,7 +69,7 @@ allowed: our UI, our/Lucide icons, OFL fonts, and decks built only from allowed or `docs/brand/` has no row. - **`NOTICE`** keeps the notices the licences require. - Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this - repo (craftrules `standards/fonts.md`). SlideCraft embeds its `fonts/latin-manifest.txt` and + repo (craftrules `standards/fonts.md`). DeckCraft embeds its `fonts/latin-manifest.txt` and `fonts/manifest.txt` when built with `CRAFT_FONTS_DIR` (local dev: `.cargo/config.toml` points at `../craft-fonts`). @@ -98,7 +98,7 @@ once and tell the user. ## 3. Never crash -People trust SlideCraft with their talks; a crash loses work. **This outranks feature work.** +People trust DeckCraft with their talks; a crash loses work. **This outranks feature work.** Standard: craftrules `standards/never-crash.md`. - No `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!` in non-test code; no `unsafe` (`unsafe_code = "forbid"`). Every production crate root carries @@ -118,11 +118,11 @@ Standard: craftrules `standards/never-crash.md`. | L1 | `model` (presentation, masters, layouts, slides, shapes, text, themes, inheritance, text edit ops) | | L2 | `fonts`, `text` (layout), | | L3 | `render` (vello_cpu) | -| L4 | `format` (.slidecraft), `pptx` | +| L4 | `format` (.deckcraft), `pptx` | | L5 | `engine` (session, history, commands, tools, keys, sample) | | L6 | `mcp` | | L7 | `ui-egui` (swappable; nothing below depends on egui/eframe/winit/rfd) | -| apps | `slidecraft` (desktop), `slidecraft-cli`, `slidecraft-web` | +| apps | `deckcraft` (desktop), `deckcraft-cli`, `deckcraft-web` | - **Everything is a command** (`crates/engine/src/cmd/*`): id, label, ribbon/menu place, shortcut, params doc, `enabled`, `run`, plus tests. Ids follow PowerPoint's ribbon and menus @@ -141,16 +141,16 @@ to `main`. ## 6. Running and looking at the app -- `cargo run --release -p slidecraft -- --sample --control 7979` (sample deck + control channel). +- `cargo run --release -p deckcraft -- --sample --control 7979` (sample deck + control channel). - Drive it: JSON lines on `127.0.0.1:7979`, e.g. `{"id":1,"method":"engine.execute","params":{"command":"shape.insert","params":{"preset":"star5","rect":[100,100,200,200]}}}` then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods: `crates/ui-egui/src/control.rs`, docs: `docs/control-protocol.md`. - **For UI work, look at the result** (screenshot, read the PNG) and compare with the observations in `plan/powerpoint/01-observed-ui.md`. -- Headless: `slidecraft-cli render --sample --all --scale 1 out/`, - `slidecraft-cli run --sample --cmd 'slide.new={"title":"Hi"}' --export out.png`. -- MCP: `slidecraft-cli mcp` (headless) or `slidecraft-cli mcp --connect 7979` (drive the app). +- Headless: `deckcraft-cli render --sample --all --scale 1 out/`, + `deckcraft-cli run --sample --cmd 'slide.new={"title":"Hi"}' --export out.png`. +- MCP: `deckcraft-cli mcp` (headless) or `deckcraft-cli mcp --connect 7979` (drive the app). See `docs/mcp.md`. - Shell gotcha on the dev machine: `mv`/`cp` are aliased interactive; use `/bin/mv -f`, `/bin/cp -f`. - Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own. diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 9ffb154..4567d53 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -1,12 +1,12 @@ # Attribution -Every asset SlideCraft includes, bundles, embeds at build time or shows in its documentation, with +Every asset DeckCraft includes, bundles, embeds at build time or shows in its documentation, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. `cargo xtask assets` fails if a file under `assets/`, `docs/images/` or `docs/brand/` is missing from this table. -SlideCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, fonts, themes, +DeckCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, fonts, themes, templates or other assets**. Its themes, colour schemes, layouts, preset-shape geometry, sample -deck and UI icons are original work of the SlideCraft contributors (drawn or generated in code). +deck and UI icons are original work of the DeckCraft contributors (drawn or generated in code). ## In this repository @@ -17,12 +17,12 @@ deck and UI icons are original work of the SlideCraft contributors (drawn or gen | `docs/brand/artcraft-mark.svg`, `docs/brand/artcraft-mark.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | README footer, About | | `docs/brand/artcraft-mark-black.svg`, `docs/brand/artcraft-mark-black.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Mark for light backgrounds | | `docs/brand/LICENSE-brand.txt` | ArtCraft Team | craftrules `standards/license-files` | — (licence text) | Terms for the ArtCraft marks | -| `assets/app-icon/slidecraft-1024.png`, `assets/app-icon/hicolor/*/apps/ai.storyteller.slidecraft.png` | SlideCraft contributors | rendered by SlideCraft from `assets/app-icon/icon-source.slidecraft` (shapes only) | MIT OR Apache-2.0 | Placeholder app icon (presentation board) until the mascot icon is drawn | -| `assets/app-icon/slidecraft.icns`, `assets/app-icon/slidecraft.ico` | SlideCraft contributors | generated from the PNGs above by `packaging/icons.sh` (sips/iconutil, `cargo xtask ico`) | MIT OR Apache-2.0 | macOS and Windows app icons | -| `assets/app-icon/icon-source.slidecraft` | SlideCraft contributors | made with `slidecraft-cli run` (see git history) | MIT OR Apache-2.0 | Source of the placeholder icon | -| `crates/media/tests/fixtures/tone*` (`tone.mp3`, `.m4a`, `.aac`, `.ogg`, `.opus`, `.flac`, `.wma`, `tone-alac.m4a`) | SlideCraft contributors | synthesised 0.5 s 440 Hz sine, encoded by `crates/media/tests/fixtures/make.sh` (ffmpeg as the encoder only) | CC0-1.0 | Audio decoding tests | -| `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp9.webm`, `clip-av1.mkv`) | SlideCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests | -| `crates/media/tests/fixtures/make.sh` | SlideCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures | +| `assets/app-icon/deckcraft-1024.png`, `assets/app-icon/hicolor/*/apps/ai.storyteller.deckcraft.png` | DeckCraft contributors | rendered by DeckCraft from `assets/app-icon/icon-source.deckcraft` (shapes only) | MIT OR Apache-2.0 | Placeholder app icon (presentation board) until the mascot icon is drawn | +| `assets/app-icon/deckcraft.icns`, `assets/app-icon/deckcraft.ico` | DeckCraft contributors | generated from the PNGs above by `packaging/icons.sh` (sips/iconutil, `cargo xtask ico`) | MIT OR Apache-2.0 | macOS and Windows app icons | +| `assets/app-icon/icon-source.deckcraft` | DeckCraft contributors | made with `deckcraft-cli run` (see git history) | MIT OR Apache-2.0 | Source of the placeholder icon | +| `crates/media/tests/fixtures/tone*` (`tone.mp3`, `.m4a`, `.aac`, `.ogg`, `.opus`, `.flac`, `.wma`, `tone-alac.m4a`) | DeckCraft contributors | synthesised 0.5 s 440 Hz sine, encoded by `crates/media/tests/fixtures/make.sh` (ffmpeg as the encoder only) | CC0-1.0 | Audio decoding tests | +| `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp9.webm`, `clip-av1.mkv`) | DeckCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests | +| `crates/media/tests/fixtures/make.sh` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures | ## Bundled through dependencies diff --git a/CLAUDE.md b/CLAUDE.md index 820317c..2dcfdbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# SlideCraft — working notes for Claude +# DeckCraft — working notes for Claude Read [AGENTS.md](AGENTS.md) first: its rules (asset policy, clean room, never crash) bind you and win over anything here. @@ -10,8 +10,8 @@ win over anything here. ## Habits - Every user-visible action is a command with tests (`crates/engine/src/cmd/*`). -- Look at UI changes: `slidecraft --sample --control 7979` + `ui.screenshot`, or - `slidecraft-cli render`. +- Look at UI changes: `deckcraft --sample --control 7979` + `ui.screenshot`, or + `deckcraft-cli render`. - Commit after each landed arc (`M.: what`) and push to `origin main`. - Keep `ROADMAP.md` and `plan/STATUS.md` current. - Don't ask the user questions; record genuinely-theirs decisions in `plan/STATUS.md`. diff --git a/Cargo.lock b/Cargo.lock index a5e6d15..6fd4c3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -864,6 +864,302 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "deckcraft" +version = "0.1.0" +dependencies = [ + "arboard", + "cpal", + "deckcraft-engine", + "deckcraft-media", + "deckcraft-ui-egui", + "eframe", + "egui", + "image", + "log", + "rfd", + "serde_json", + "winit", + "winresource", +] + +[[package]] +name = "deckcraft-anim" +version = "0.1.0" +dependencies = [ + "deckcraft-geom", + "deckcraft-model", + "serde", +] + +[[package]] +name = "deckcraft-av1" +version = "0.1.0" +dependencies = [ + "deckcraft-bitstream", + "rayon", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-bitstream" +version = "0.1.0" +dependencies = [ + "proptest", +] + +[[package]] +name = "deckcraft-cli" +version = "0.1.0" +dependencies = [ + "deckcraft-engine", + "deckcraft-mcp", + "serde_json", +] + +[[package]] +name = "deckcraft-color" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "deckcraft-engine" +version = "0.1.0" +dependencies = [ + "deckcraft-color", + "deckcraft-fonts", + "deckcraft-format", + "deckcraft-geom", + "deckcraft-media", + "deckcraft-model", + "deckcraft-pdf", + "deckcraft-pptx", + "deckcraft-render", + "deckcraft-text", + "i_overlay", + "image", + "log", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-fonts" +version = "0.1.0" +dependencies = [ + "epaint_default_fonts", + "harfrust 0.13.3", + "kurbo", + "log", + "skrifa 0.47.0", + "unicode-bidi", +] + +[[package]] +name = "deckcraft-format" +version = "0.1.0" +dependencies = [ + "deckcraft-model", + "serde", + "serde_json", + "thiserror 2.0.21", + "zip", +] + +[[package]] +name = "deckcraft-geom" +version = "0.1.0" +dependencies = [ + "kurbo", + "serde", +] + +[[package]] +name = "deckcraft-h264" +version = "0.1.0" +dependencies = [ + "deckcraft-bitstream", + "rayon", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-hevc" +version = "0.1.0" +dependencies = [ + "deckcraft-bitstream", + "rayon", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-isobmff" +version = "0.1.0" +dependencies = [ + "deckcraft-bitstream", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-matroska" +version = "0.1.0" + +[[package]] +name = "deckcraft-mcp" +version = "0.1.0" +dependencies = [ + "deckcraft-engine", + "deckcraft-render", + "log", + "serde_json", +] + +[[package]] +name = "deckcraft-media" +version = "0.1.0" +dependencies = [ + "deckcraft-av1", + "deckcraft-h264", + "deckcraft-hevc", + "deckcraft-isobmff", + "deckcraft-matroska", + "deckcraft-ogg", + "deckcraft-opus", + "deckcraft-vp9", + "image", + "log", + "symphonia", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-model" +version = "0.1.0" +dependencies = [ + "deckcraft-color", + "deckcraft-geom", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-ogg" +version = "0.1.0" + +[[package]] +name = "deckcraft-opus" +version = "0.1.0" + +[[package]] +name = "deckcraft-pdf" +version = "0.1.0" +dependencies = [ + "deckcraft-fonts", + "deckcraft-model", + "deckcraft-render", + "deckcraft-text", + "krilla", + "kurbo", + "log", + "serde", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-pptx" +version = "0.1.0" +dependencies = [ + "deckcraft-color", + "deckcraft-engine", + "deckcraft-geom", + "deckcraft-model", + "image", + "log", + "quick-xml 0.38.4", + "serde_json", + "thiserror 2.0.21", + "zip", +] + +[[package]] +name = "deckcraft-render" +version = "0.1.0" +dependencies = [ + "deckcraft-color", + "deckcraft-fonts", + "deckcraft-geom", + "deckcraft-model", + "deckcraft-text", + "image", + "kurbo", + "log", + "vello_common 0.2.0", + "vello_cpu 0.2.0", +] + +[[package]] +name = "deckcraft-text" +version = "0.1.0" +dependencies = [ + "deckcraft-color", + "deckcraft-fonts", + "deckcraft-geom", + "deckcraft-model", + "unicode-linebreak", + "unicode-segmentation", +] + +[[package]] +name = "deckcraft-ui-egui" +version = "0.1.0" +dependencies = [ + "deckcraft-anim", + "deckcraft-color", + "deckcraft-engine", + "deckcraft-fonts", + "deckcraft-geom", + "deckcraft-media", + "deckcraft-model", + "deckcraft-render", + "deckcraft-text", + "egui", + "egui_kittest", + "js-sys", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "deckcraft-vp9" +version = "0.1.0" +dependencies = [ + "deckcraft-bitstream", + "rayon", + "thiserror 2.0.21", +] + +[[package]] +name = "deckcraft-web" +version = "0.1.0" +dependencies = [ + "deckcraft-engine", + "deckcraft-ui-egui", + "eframe", + "egui", + "js-sys", + "log", + "rfd", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "derive_arbitrary" version = "1.5.0" @@ -3447,302 +3743,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" -[[package]] -name = "slidecraft" -version = "0.1.0" -dependencies = [ - "arboard", - "cpal", - "eframe", - "egui", - "image", - "log", - "rfd", - "serde_json", - "slidecraft-engine", - "slidecraft-media", - "slidecraft-ui-egui", - "winit", - "winresource", -] - -[[package]] -name = "slidecraft-anim" -version = "0.1.0" -dependencies = [ - "serde", - "slidecraft-geom", - "slidecraft-model", -] - -[[package]] -name = "slidecraft-av1" -version = "0.1.0" -dependencies = [ - "rayon", - "slidecraft-bitstream", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-bitstream" -version = "0.1.0" -dependencies = [ - "proptest", -] - -[[package]] -name = "slidecraft-cli" -version = "0.1.0" -dependencies = [ - "serde_json", - "slidecraft-engine", - "slidecraft-mcp", -] - -[[package]] -name = "slidecraft-color" -version = "0.1.0" -dependencies = [ - "serde", -] - -[[package]] -name = "slidecraft-engine" -version = "0.1.0" -dependencies = [ - "i_overlay", - "image", - "log", - "serde", - "serde_json", - "slidecraft-color", - "slidecraft-fonts", - "slidecraft-format", - "slidecraft-geom", - "slidecraft-media", - "slidecraft-model", - "slidecraft-pdf", - "slidecraft-pptx", - "slidecraft-render", - "slidecraft-text", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-fonts" -version = "0.1.0" -dependencies = [ - "epaint_default_fonts", - "harfrust 0.13.3", - "kurbo", - "log", - "skrifa 0.47.0", - "unicode-bidi", -] - -[[package]] -name = "slidecraft-format" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "slidecraft-model", - "thiserror 2.0.21", - "zip", -] - -[[package]] -name = "slidecraft-geom" -version = "0.1.0" -dependencies = [ - "kurbo", - "serde", -] - -[[package]] -name = "slidecraft-h264" -version = "0.1.0" -dependencies = [ - "rayon", - "slidecraft-bitstream", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-hevc" -version = "0.1.0" -dependencies = [ - "rayon", - "slidecraft-bitstream", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-isobmff" -version = "0.1.0" -dependencies = [ - "slidecraft-bitstream", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-matroska" -version = "0.1.0" - -[[package]] -name = "slidecraft-mcp" -version = "0.1.0" -dependencies = [ - "log", - "serde_json", - "slidecraft-engine", - "slidecraft-render", -] - -[[package]] -name = "slidecraft-media" -version = "0.1.0" -dependencies = [ - "image", - "log", - "slidecraft-av1", - "slidecraft-h264", - "slidecraft-hevc", - "slidecraft-isobmff", - "slidecraft-matroska", - "slidecraft-ogg", - "slidecraft-opus", - "slidecraft-vp9", - "symphonia", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-model" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "slidecraft-color", - "slidecraft-geom", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-ogg" -version = "0.1.0" - -[[package]] -name = "slidecraft-opus" -version = "0.1.0" - -[[package]] -name = "slidecraft-pdf" -version = "0.1.0" -dependencies = [ - "krilla", - "kurbo", - "log", - "serde", - "slidecraft-fonts", - "slidecraft-model", - "slidecraft-render", - "slidecraft-text", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-pptx" -version = "0.1.0" -dependencies = [ - "image", - "log", - "quick-xml 0.38.4", - "serde_json", - "slidecraft-color", - "slidecraft-engine", - "slidecraft-geom", - "slidecraft-model", - "thiserror 2.0.21", - "zip", -] - -[[package]] -name = "slidecraft-render" -version = "0.1.0" -dependencies = [ - "image", - "kurbo", - "log", - "slidecraft-color", - "slidecraft-fonts", - "slidecraft-geom", - "slidecraft-model", - "slidecraft-text", - "vello_common 0.2.0", - "vello_cpu 0.2.0", -] - -[[package]] -name = "slidecraft-text" -version = "0.1.0" -dependencies = [ - "slidecraft-color", - "slidecraft-fonts", - "slidecraft-geom", - "slidecraft-model", - "unicode-linebreak", - "unicode-segmentation", -] - -[[package]] -name = "slidecraft-ui-egui" -version = "0.1.0" -dependencies = [ - "egui", - "egui_kittest", - "js-sys", - "log", - "serde", - "serde_json", - "slidecraft-anim", - "slidecraft-color", - "slidecraft-engine", - "slidecraft-fonts", - "slidecraft-geom", - "slidecraft-media", - "slidecraft-model", - "slidecraft-render", - "slidecraft-text", -] - -[[package]] -name = "slidecraft-vp9" -version = "0.1.0" -dependencies = [ - "rayon", - "slidecraft-bitstream", - "thiserror 2.0.21", -] - -[[package]] -name = "slidecraft-web" -version = "0.1.0" -dependencies = [ - "eframe", - "egui", - "js-sys", - "log", - "rfd", - "serde_json", - "slidecraft-engine", - "slidecraft-ui-egui", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "slotmap" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index c27dc37..50aeb93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,41 +1,41 @@ [workspace] resolver = "3" members = ["crates/*", "apps/*", "xtask"] -default-members = ["crates/*", "apps/slidecraft", "apps/slidecraft-cli"] +default-members = ["crates/*", "apps/deckcraft", "apps/deckcraft-cli"] [workspace.package] version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.90" -repository = "https://github.com/storytold/slidecraft" +repository = "https://github.com/storytold/deckcraft" [workspace.dependencies] # internal -slidecraft-geom = { path = "crates/geom" } -slidecraft-color = { path = "crates/color" } -slidecraft-model = { path = "crates/model" } -slidecraft-fonts = { path = "crates/fonts" } -slidecraft-text = { path = "crates/text" } -slidecraft-render = { path = "crates/render" } -slidecraft-pdf = { path = "crates/pdf" } -slidecraft-pptx = { path = "crates/pptx" } -slidecraft-format = { path = "crates/format" } -slidecraft-anim = { path = "crates/anim" } -slidecraft-engine = { path = "crates/engine" } -slidecraft-mcp = { path = "crates/mcp" } -slidecraft-ui-egui = { path = "crates/ui-egui" } -slidecraft-media = { path = "crates/media" } +deckcraft-geom = { path = "crates/geom" } +deckcraft-color = { path = "crates/color" } +deckcraft-model = { path = "crates/model" } +deckcraft-fonts = { path = "crates/fonts" } +deckcraft-text = { path = "crates/text" } +deckcraft-render = { path = "crates/render" } +deckcraft-pdf = { path = "crates/pdf" } +deckcraft-pptx = { path = "crates/pptx" } +deckcraft-format = { path = "crates/format" } +deckcraft-anim = { path = "crates/anim" } +deckcraft-engine = { path = "crates/engine" } +deckcraft-mcp = { path = "crates/mcp" } +deckcraft-ui-egui = { path = "crates/ui-egui" } +deckcraft-media = { path = "crates/media" } # codecs (ported from FilmCraft, same owner and licence) -slidecraft-bitstream = { path = "crates/bitstream" } -slidecraft-h264 = { path = "crates/h264" } -slidecraft-hevc = { path = "crates/hevc" } -slidecraft-vp9 = { path = "crates/vp9" } -slidecraft-av1 = { path = "crates/av1" } -slidecraft-isobmff = { path = "crates/isobmff" } -slidecraft-matroska = { path = "crates/matroska" } -slidecraft-ogg = { path = "crates/ogg" } -slidecraft-opus = { path = "crates/opus" } +deckcraft-bitstream = { path = "crates/bitstream" } +deckcraft-h264 = { path = "crates/h264" } +deckcraft-hevc = { path = "crates/hevc" } +deckcraft-vp9 = { path = "crates/vp9" } +deckcraft-av1 = { path = "crates/av1" } +deckcraft-isobmff = { path = "crates/isobmff" } +deckcraft-matroska = { path = "crates/matroska" } +deckcraft-ogg = { path = "crates/ogg" } +deckcraft-opus = { path = "crates/opus" } # foundation kurbo = { version = "0.13", features = ["serde"] } diff --git a/LICENSE-MIT b/LICENSE-MIT index d973456..326ccf1 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 ArtCraft Team and the SlideCraft contributors +Copyright (c) 2026 ArtCraft Team and the DeckCraft contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NOTICE b/NOTICE index 483fbba..fb209cf 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,5 @@ -SlideCraft -Copyright (c) 2026 ArtCraft Team and the SlideCraft contributors. +DeckCraft +Copyright (c) 2026 ArtCraft Team and the DeckCraft contributors. Licensed under the MIT License or the Apache License, Version 2.0, at your option (LICENSE-MIT, LICENSE-APACHE). diff --git a/README.md b/README.md index 0ee333c..a73c69d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

-

SlideCraft

+

DeckCraft

Presentations and slide shows; an open-source, clean-room reimplementation of Microsoft PowerPoint, rebuilt in pure Rust. @@ -32,7 +32,7 @@

- SlideCraft on getartcraft.com · + DeckCraft on getartcraft.com · ArtCraft · All Crafting Apps

@@ -53,7 +53,7 @@ ## What works today -SlideCraft is in early development. Today: +DeckCraft is in early development. Today: - **Presentation model** with slide masters, eleven layouts, placeholders that inherit position and formatting, themes (colour, font and effect schemes), sections, notes, comments, transitions and @@ -70,25 +70,25 @@ SlideCraft is in early development. Today: ## Getting started ```sh -git clone https://github.com/storytold/slidecraft -cd slidecraft -cargo run --release -p slidecraft-cli -- render --sample --all --scale 1 out/ +git clone https://github.com/storytold/deckcraft +cd deckcraft +cargo run --release -p deckcraft-cli -- render --sample --all --scale 1 out/ ``` Fonts come from [craft-fonts](https://github.com/storytold/craft-fonts): clone it next to this repo -(`../craft-fonts`) and local builds pick it up automatically; without it SlideCraft uses your system +(`../craft-fonts`) and local builds pick it up automatically; without it DeckCraft uses your system fonts. ## Agents, CLI and MCP -Every action in SlideCraft is a command with a stable id, so people, scripts and AI agents use the +Every action in DeckCraft is a command with a stable id, so people, scripts and AI agents use the same verbs: ```sh -slidecraft-cli commands format. # list commands -slidecraft-cli run --sample --cmd 'slide.new={"layout":"titleOnly","title":"Hello"}' --save hello.slidecraft -slidecraft-cli mcp # MCP server over stdio (headless) -slidecraft-cli mcp --connect 7979 # drive the running app (slidecraft --control 7979) +deckcraft-cli commands format. # list commands +deckcraft-cli run --sample --cmd 'slide.new={"layout":"titleOnly","title":"Hello"}' --save hello.deckcraft +deckcraft-cli mcp # MCP server over stdio (headless) +deckcraft-cli mcp --connect 7979 # drive the running app (deckcraft --control 7979) ``` ## Roadmap @@ -97,7 +97,7 @@ See [ROADMAP.md](ROADMAP.md) for milestones, parity estimates and what's next. ## The Crafting Apps -SlideCraft is one of the **Crafting Apps**: free, open-source creative tools from the +DeckCraft is one of the **Crafting Apps**: free, open-source creative tools from the [ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to stand on its own. @@ -110,7 +110,7 @@ stand on its own. | | **PrintCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/printcraft) | [Website](https://getartcraft.com/apps/printcraft) | | | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) | | | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) | -| | **SlideCraft** | **Presentations and slide shows · you are here** | [GitHub](https://github.com/storytold/slidecraft) | [Website](https://getartcraft.com/apps/slidecraft) | +| | **DeckCraft** | **Presentations and slide shows · you are here** | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) | And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control. @@ -133,26 +133,26 @@ And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video stud discord.gg/artcraft · getartcraft.com · The Crafting Apps · - SlideCraft + DeckCraft

## License and credits -SlideCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option. -Copyright (c) 2026 ArtCraft Team and the SlideCraft contributors. Required notices are in [NOTICE](NOTICE). +DeckCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option. +Copyright (c) 2026 ArtCraft Team and the DeckCraft contributors. Required notices are in [NOTICE](NOTICE). Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md). -SlideCraft's themes, layouts, shape geometry, icons and sample decks are original work, drawn or +DeckCraft's themes, layouts, shape geometry, icons and sample decks are original work, drawn or generated in code; no Microsoft artwork, fonts, themes or templates are used. The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as -part of this repository and SlideCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). +part of this repository and DeckCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). Forks and modified versions must remove them. -Microsoft and PowerPoint are trademarks of the Microsoft group of companies. Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. SlideCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Microsoft Corporation or Adobe Inc.; these names are used only to describe the workflows it is compatible with. +Microsoft and PowerPoint are trademarks of the Microsoft group of companies. Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. DeckCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Microsoft Corporation or Adobe Inc.; these names are used only to describe the workflows it is compatible with.

ArtCraft
diff --git a/ROADMAP.md b/ROADMAP.md index 4cd7b03..a8f0287 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ -# SlideCraft roadmap +# DeckCraft roadmap -SlideCraft aims at full PowerPoint parity — and to be better: faster, open (a documented zip+JSON +DeckCraft aims at full PowerPoint parity — and to be better: faster, open (a documented zip+JSON format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere (macOS, Windows, Linux, BSD and the web). @@ -34,46 +34,67 @@ format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and av - PDF export: slides, notes pages and handouts (1–9 per page) with a selectable real-text layer, hyperlinks and slide bookmarks (`file.export {format: "pdf", layout}`, File › Export…). - UI: PowerPoint-style ribbon with contextual tabs, ~240 original icons, status bar, panes, command palette, light/dark. -- Automation: ~200 commands, every one reachable from `slidecraft-cli`, the app's JSON control +- Automation: ~200 commands, every one reachable from `deckcraft-cli`, the app's JSON control channel and the MCP server (headless or connected to the running app). -- Web: `apps/slidecraft-web` runs the same UI in the browser (trunk; WebGPU with WebGL2 fallback), +- Web: `apps/deckcraft-web` runs the same UI in the browser (trunk; WebGPU with WebGL2 fallback), opening the sample deck; Open/Insert use the browser file picker, Save/Export download. - Release CI: pushes to `release` build a draft GitHub Release (macOS universal dmg, Windows x64/x86 msi + zip, Linux AppImage/deb/rpm/tar.gz + Flatpak, FreeBSD tar.gz, web zip); signing secrets live in the `release` environment. Version: `cargo xtask version`. -**In progress:** PPTX import/export · first signed release run. - -**Next (in order):** vector PDF artwork · WMA/WMV decoding · Format Shape pane depth · connectors with -glue, freeform and edit points · merge shapes · Animation Pane polish · native macOS menu bar · -print · Notes/Handout masters · equations · SVG pictures. +**Next (in order):** first signed release run · PPTX corpus hardening against real-world decks · +Animation Pane and presenter view polish · vector PDF artwork · Format Shape pane depth (3-D, +picture/texture options) · edit points · native macOS menu bar · print · Notes/Handout masters · +equations · SVG pictures · WMA/WMV decoding. Milestone details live in `plan/execution-plan.md` (M0–M14, local planning notes). +## How close to an alpha (estimate, 2026-10-07) + +An **alpha** here means: someone can install a signed build on macOS, Windows or Linux, make a real +deck from scratch or from a PowerPoint file, present it with transitions, animations and media, +save it back to `.pptx`/PDF without losing work, and hit no crashes on the common paths. + +**DeckCraft is about 80% of the way to that alpha — roughly 30–40 wall-clock hours of a single +Claude Opus 5.5 agent** (about 12–18 hours with three agents in parallel). + +| Alpha blocker | State | Estimate | +|---|---|---| +| First real release run: signing, notarization, installers verified on each OS | Workflows written and linted, never run | 6 h | +| PPTX fidelity on a corpus of real decks (import, round-trip, opens without repair) | Verified on generated decks only | 8 h | +| Presenter view and Animation Pane polish (timeline, reorder, preview) | Working, rough | 6 h | +| Soak and fuzz the editor (random command sequences, big decks, undo/redo) for crashes and slowness | Unit tests and guards only | 5 h | +| UI fidelity pass on the most-used ribbon groups and dialogs; first-run experience | Mostly there | 6 h | +| Mascot app icon (owner), README/site screenshots, user docs | Placeholder icon | 3 h | + +Already alpha-ready: editing, text, shapes (incl. connectors, freeform, merge), themes and +backgrounds, tables, charts, transitions, animations, slide show, audio/video playback, PDF export, +AutoRecover, CLI/MCP automation, web build. + ## How far from full parity (estimate, 2026-10-07) -**Breadth: ~74% weighted** (P0 core 88%, P1 67%, P2 33%) over the 187 features of the PowerPoint -catalogue, scored row by row in [docs/parity.md](docs/parity.md) (`cargo xtask parity` recomputes -it). Many features scored done still lack some of PowerPoint's options, dialogs or pixel fidelity, so -**overall parity including depth is about 55%**. +**Breadth: ~79% weighted** (P0 core 93%, P1 74%, P2 35%) over the 187 features of the PowerPoint catalogue, scored row by +row in [docs/parity.md](docs/parity.md) (`cargo xtask parity` recomputes it). Many features scored +done still lack some of PowerPoint's options, dialogs or pixel fidelity, so **overall parity +including depth is about 62%**. -**Remaining work: about 230 wall-clock hours of a single Claude Opus 5.5 agent** (±30%), or roughly -80–110 hours with four agents in parallel on separate crates: +**Remaining work to 100%: about 190 wall-clock hours of a single Claude Opus 5.5 agent** (±30%), +or roughly 65–90 hours with four agents in parallel on separate crates: | Work | Estimate | |---|---| -| Open P0 (16 partial, 1 missing: PPTX fidelity, vector PDF, media playback, Format Shape pane, presenter view…) | 30 h | -| Open P1 (23 partial, 10 missing: connectors/glue, freeform, merge shapes, print, multi-monitor, SVG…) | 45 h | -| Open P2 (11 partial, 28 missing: equations, video export, 3-D, remove background, thesaurus…) | 40 h | -| Depth and pixel fidelity of every ribbon group, dialog and pane against PowerPoint | 75 h | -| Performance (incremental rendering, GPU raster) and PPTX corpus hardening | 30 h | -| Platform verification and release hardening (signing, Flatpak, BSD, web) | 10 h | +| Alpha blockers above | 35 h | +| Open P0/P1 rows (vector PDF, print, edit points, SVG, multi-monitor, draw table, SmartArt text pane, Notes/Handout masters…) | 45 h | +| Open P2 rows (equations, video export, 3-D, remove background, thesaurus, compare, record show…) | 40 h | +| Depth and pixel fidelity of every ribbon group, dialog and pane against PowerPoint | 50 h | +| Performance (incremental rendering, GPU raster, streaming media decode) | 20 h | -Basis: the M0 arc (model, renderer, text engine, engine with ~200 commands, ribbon UI, show engine, -CLI and MCP) took roughly 25 wall-clock agent hours; open catalogue rows average 1–2 hours each. +Basis: in this session the engine, renderer, UI, show engine, PPTX, PDF, media and release +pipeline were built in about 30 wall-clock hours with up to three agents in parallel; recent +catalogue rows (connectors, freeform, merge shapes, AutoRecover, gradients) took 1–2 hours each. ## Agents: CLI and MCP -Every command is reachable from `slidecraft-cli` (`run`, `describe`, `commands`, `app` for the running -window, `render`, `convert`), from MCP (`slidecraft-cli mcp`, optionally `--connect PORT`), and from the -app's JSON control channel (`slidecraft --control PORT`). +Every command is reachable from `deckcraft-cli` (`run`, `describe`, `commands`, `app` for the running +window, `render`, `convert`), from MCP (`deckcraft-cli mcp`, optionally `--connect PORT`), and from the +app's JSON control channel (`deckcraft --control PORT`). diff --git a/apps/deckcraft-cli/Cargo.toml b/apps/deckcraft-cli/Cargo.toml new file mode 100644 index 0000000..898a1fb --- /dev/null +++ b/apps/deckcraft-cli/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "deckcraft-cli" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "DeckCraft on the command line: render, convert, run commands, inspect, and serve MCP" +publish = false + +[lints] +workspace = true + +[[bin]] +name = "deckcraft-cli" +path = "src/main.rs" + +[dependencies] +deckcraft-engine = { workspace = true } +deckcraft-mcp = { workspace = true } +serde_json = { workspace = true } diff --git a/apps/slidecraft-cli/src/main.rs b/apps/deckcraft-cli/src/main.rs similarity index 78% rename from apps/slidecraft-cli/src/main.rs rename to apps/deckcraft-cli/src/main.rs index be37bc7..c450393 100644 --- a/apps/slidecraft-cli/src/main.rs +++ b/apps/deckcraft-cli/src/main.rs @@ -1,23 +1,23 @@ -//! Headless SlideCraft. +//! Headless DeckCraft. //! //! ```text -//! slidecraft-cli info FILE # slides, titles, layouts (JSON) -//! slidecraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] OUT.png|OUT_DIR -//! slidecraft-cli convert IN OUT # .slidecraft ⇄ .pptx, outline .txt, .png (first slide) -//! slidecraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print] -//! slidecraft-cli commands [FILTER] # list commands (JSON) -//! slidecraft-cli describe ID # one command -//! slidecraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] # drive the running app -//! slidecraft-cli mcp [--connect PORT] [--sample] # MCP server over stdio -//! slidecraft-cli links | --version +//! deckcraft-cli info FILE # slides, titles, layouts (JSON) +//! deckcraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] OUT.png|OUT_DIR +//! deckcraft-cli convert IN OUT # .deckcraft ⇄ .pptx, outline .txt, .png (first slide) +//! deckcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print] +//! deckcraft-cli commands [FILTER] # list commands (JSON) +//! deckcraft-cli describe ID # one command +//! deckcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] # drive the running app +//! deckcraft-cli mcp [--connect PORT] [--sample] # MCP server over stdio +//! deckcraft-cli links | --version //! ``` #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] use std::process::ExitCode; +use deckcraft_engine::Session; use serde_json::{Value, json}; -use slidecraft_engine::Session; macro_rules! outln { ($($arg:tt)*) => {{ @@ -26,28 +26,28 @@ macro_rules! outln { if e.kind() == std::io::ErrorKind::BrokenPipe { std::process::exit(0); } - eprintln!("slidecraft-cli: can't write to stdout: {e}"); + eprintln!("deckcraft-cli: can't write to stdout: {e}"); std::process::exit(1); } }}; } -const USAGE: &str = "usage: slidecraft-cli info FILE - slidecraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] OUT - slidecraft-cli convert IN OUT - slidecraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print] - slidecraft-cli commands [FILTER] - slidecraft-cli describe COMMAND - slidecraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] - slidecraft-cli mcp [--connect PORT] [--sample] - slidecraft-cli links | --version"; +const USAGE: &str = "usage: deckcraft-cli info FILE + deckcraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] OUT + deckcraft-cli convert IN OUT + deckcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print] + deckcraft-cli commands [FILTER] + deckcraft-cli describe COMMAND + deckcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] + deckcraft-cli mcp [--connect PORT] [--sample] + deckcraft-cli links | --version"; fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); let rest = args.get(1..).unwrap_or(&[]); match args.first().map(String::as_str) { Some("--version" | "-V" | "version") => { - outln!("slidecraft-cli {}", env!("CARGO_PKG_VERSION")); + outln!("deckcraft-cli {}", env!("CARGO_PKG_VERSION")); ExitCode::SUCCESS } Some("info") => report(info(rest)), @@ -70,13 +70,13 @@ fn main() -> ExitCode { Some("app") => report(app(rest)), Some("mcp") => report(mcp(rest)), Some("links") => { - use slidecraft_engine::links::*; + use deckcraft_engine::links::*; outln!("Discord {DISCORD}\nWebsite {WEBSITE}\nApp page {APP_PAGE}\nGitHub {GITHUB}"); ExitCode::SUCCESS } _ => { eprintln!("{USAGE}"); - eprintln!("\nCommunity: {} · {}", slidecraft_engine::links::DISCORD, slidecraft_engine::links::APP_PAGE); + eprintln!("\nCommunity: {} · {}", deckcraft_engine::links::DISCORD, deckcraft_engine::links::APP_PAGE); ExitCode::FAILURE } } @@ -86,7 +86,7 @@ fn report(r: Result<(), String>) -> ExitCode { match r { Ok(()) => ExitCode::SUCCESS, Err(e) => { - eprintln!("slidecraft-cli: {e}"); + eprintln!("deckcraft-cli: {e}"); ExitCode::FAILURE } } @@ -94,7 +94,7 @@ fn report(r: Result<(), String>) -> ExitCode { fn open(s: &mut Session, input: Option<&str>, sample: bool) -> Result<(), String> { if sample { - return slidecraft_engine::sample::open_sample(s).map_err(|e| e.to_string()); + return deckcraft_engine::sample::open_sample(s).map_err(|e| e.to_string()); } match input { Some(p) => s.execute("file.open", &json!({"path": p})).map(|_| ()).map_err(|e| e.to_string()), @@ -153,7 +153,7 @@ fn render(args: &[String]) -> Result<(), String> { if i >= n { return Err(format!("no slide {i} (the deck has {n})")); } - let (png, w, h) = slidecraft_engine::cmd::file::render_png(&st.doc, i, scale, edit); + let (png, w, h) = deckcraft_engine::cmd::file::render_png(&st.doc, i, scale, edit); let path = if all { format!("{out}/slide-{:02}.png", i + 1) } else { out.clone() }; std::fs::write(&path, png).map_err(|e| format!("{path}: {e}"))?; outln!("{path} ({w}×{h})"); @@ -165,7 +165,7 @@ fn convert(args: &[String]) -> Result<(), String> { let (Some(input), Some(out)) = (args.first(), args.get(1)) else { return Err("convert needs IN and OUT".into()) }; let mut s = Session::new(); open(&mut s, Some(input), false)?; - let fmt = slidecraft_engine::cmd::file::format_for_path(out); + let fmt = deckcraft_engine::cmd::file::format_for_path(out); let r = if matches!(fmt, "png" | "jpeg") { s.execute("file.export", &json!({"path": out, "slide": 0})) } else { @@ -218,14 +218,14 @@ fn run(args: &[String]) -> Result<(), String> { fn describe(id: Option<&str>) -> Result<(), String> { let id = id.ok_or("describe needs a command id")?; - let c = slidecraft_engine::find_command(id).ok_or_else(|| format!("unknown command `{id}`"))?; + let c = deckcraft_engine::find_command(id).ok_or_else(|| format!("unknown command `{id}`"))?; outln!("{} — {}\n where: {}\n shortcut: {}\n params: {}", c.id, c.label, c.menu.join(" ▸ "), c.shortcut.unwrap_or("—"), c.params); Ok(()) } fn app(args: &[String]) -> Result<(), String> { - use slidecraft_mcp::{Backend, Remote, control_addr}; - let mut port = slidecraft_mcp::DEFAULT_PORT.to_string(); + use deckcraft_mcp::{Backend, Remote, control_addr}; + let mut port = deckcraft_mcp::DEFAULT_PORT.to_string(); let mut method: Option = None; let mut rest = vec![]; let mut it = args.iter(); @@ -237,7 +237,7 @@ fn app(args: &[String]) -> Result<(), String> { } } let mut r = Remote::connect(&control_addr(&port)) - .map_err(|e| format!("no SlideCraft app on port {port} ({e}); start it with `slidecraft --control {port}`"))?; + .map_err(|e| format!("no DeckCraft app on port {port} ({e}); start it with `deckcraft --control {port}`"))?; let parse = |s: Option<&String>| -> Result { s.map(|j| serde_json::from_str(j).map_err(|e| format!("bad JSON: {e}"))).transpose().map(|v| v.unwrap_or(json!({}))) }; @@ -253,7 +253,7 @@ fn app(args: &[String]) -> Result<(), String> { } fn mcp(args: &[String]) -> Result<(), String> { - use slidecraft_mcp::{Backend, Headless, Remote, Server, control_addr}; + use deckcraft_mcp::{Backend, Headless, Remote, Server, control_addr}; let mut connect: Option = None; let mut sample = false; let mut it = args.iter(); @@ -269,7 +269,7 @@ fn mcp(args: &[String]) -> Result<(), String> { None => { let mut h = Headless::new(); if sample { - slidecraft_engine::sample::open_sample(&mut h.session).map_err(|e| e.to_string())?; + deckcraft_engine::sample::open_sample(&mut h.session).map_err(|e| e.to_string())?; } Box::new(h) } diff --git a/apps/slidecraft-web/Cargo.toml b/apps/deckcraft-web/Cargo.toml similarity index 77% rename from apps/slidecraft-web/Cargo.toml rename to apps/deckcraft-web/Cargo.toml index 16b8546..7014421 100644 --- a/apps/slidecraft-web/Cargo.toml +++ b/apps/deckcraft-web/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "slidecraft-web" -description = "SlideCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)" +name = "deckcraft-web" +description = "DeckCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)" version.workspace = true edition.workspace = true license.workspace = true @@ -10,12 +10,12 @@ publish = false workspace = true [[bin]] -name = "slidecraft-web" +name = "deckcraft-web" path = "src/main.rs" [dependencies] -slidecraft-engine = { workspace = true } -slidecraft-ui-egui = { workspace = true } +deckcraft-engine = { workspace = true } +deckcraft-ui-egui = { workspace = true } egui = { workspace = true } log = { workspace = true } serde_json = { workspace = true } diff --git a/apps/slidecraft-web/Trunk.toml b/apps/deckcraft-web/Trunk.toml similarity index 67% rename from apps/slidecraft-web/Trunk.toml rename to apps/deckcraft-web/Trunk.toml index c6e5c00..dac89ec 100644 --- a/apps/slidecraft-web/Trunk.toml +++ b/apps/deckcraft-web/Trunk.toml @@ -1,4 +1,4 @@ -# Build with: trunk build --release (from apps/slidecraft-web) +# Build with: trunk build --release (from apps/deckcraft-web) [build] target = "index.html" dist = "../../dist/web" diff --git a/apps/deckcraft-web/index.html b/apps/deckcraft-web/index.html new file mode 100644 index 0000000..b1834c3 --- /dev/null +++ b/apps/deckcraft-web/index.html @@ -0,0 +1,20 @@ + + + + + + DeckCraft + + + + + + +

Loading DeckCraft…
+ + diff --git a/apps/slidecraft-web/src/main.rs b/apps/deckcraft-web/src/main.rs similarity index 82% rename from apps/slidecraft-web/src/main.rs rename to apps/deckcraft-web/src/main.rs index 82f564c..366ea34 100644 --- a/apps/slidecraft-web/src/main.rs +++ b/apps/deckcraft-web/src/main.rs @@ -1,6 +1,6 @@ -//! SlideCraft in the browser. +//! DeckCraft in the browser. //! -//! Runs the same [`slidecraft_ui_egui::SlideApp`] as the desktop app through eframe's web +//! Runs the same [`deckcraft_ui_egui::SlideApp`] as the desktop app through eframe's web //! runner (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` //! from this directory (output in `dist/web`). //! @@ -27,5 +27,5 @@ fn main() { #[cfg(not(target_arch = "wasm32"))] fn main() { - eprintln!("slidecraft-web only runs in the browser: build it with `trunk build --release` in apps/slidecraft-web"); + eprintln!("deckcraft-web only runs in the browser: build it with `trunk build --release` in apps/deckcraft-web"); } diff --git a/apps/slidecraft-web/src/web.rs b/apps/deckcraft-web/src/web.rs similarity index 90% rename from apps/slidecraft-web/src/web.rs rename to apps/deckcraft-web/src/web.rs index 0bb14b7..f0199a5 100644 --- a/apps/slidecraft-web/src/web.rs +++ b/apps/deckcraft-web/src/web.rs @@ -1,15 +1,15 @@ //! The browser shell: web `Services`, drag-and-drop, and the eframe web runner. -use slidecraft_engine::Session; -use slidecraft_ui_egui::{Inbox, Services, SlideApp}; +use deckcraft_engine::Session; +use deckcraft_ui_egui::{Inbox, Services, SlideApp}; use wasm_bindgen::JsCast as _; -const PRESENTATION_EXTS: &[&str] = &["slidecraft", "pptx", "potx", "ppsx", "txt", "md"]; +const PRESENTATION_EXTS: &[&str] = &["deckcraft", "pptx", "potx", "ppsx", "txt", "md"]; const PICTURE_EXTS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff"]; const AUDIO_EXTS: &[&str] = &["wav", "mp3", "m4a", "aac", "flac", "ogg", "opus", "aif", "aiff"]; const VIDEO_EXTS: &[&str] = &["mp4", "m4v", "mov", "webm"]; -const CANVAS_ID: &str = "slidecraft_canvas"; -const LOADING_ID: &str = "slidecraft_loading"; +const CANVAS_ID: &str = "deckcraft_canvas"; +const LOADING_ID: &str = "deckcraft_loading"; pub fn start() { eframe::WebLogger::init(log::LevelFilter::Info).ok(); @@ -34,13 +34,13 @@ pub fn start() { options, Box::new(move |cc| { if let Some(rs) = &cc.wgpu_render_state { - log::info!("slidecraft-web {}: wgpu backend {:?}", env!("CARGO_PKG_VERSION"), rs.adapter.get_info().backend); + log::info!("deckcraft-web {}: wgpu backend {:?}", env!("CARGO_PKG_VERSION"), rs.adapter.get_info().backend); } let inbox: Inbox = Inbox::default(); let mut app = SlideApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone())); let q = query(); if !q.contains("blank") { - if let Err(e) = slidecraft_engine::sample::open_sample(&mut app.session) { + if let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) { log::error!("sample deck: {e}"); } } else if let Err(e) = app.run("file.new", serde_json::json!({})) { @@ -56,7 +56,7 @@ pub fn start() { if let Some(el) = document.get_element_by_id(LOADING_ID) { match result { Ok(()) => el.remove(), - Err(e) => el.set_inner_html(&format!("

SlideCraft failed to start: {e:?}

A browser with WebGPU or WebGL2 is required.

")), + Err(e) => el.set_inner_html(&format!("

DeckCraft failed to start: {e:?}

A browser with WebGPU or WebGL2 is required.

")), } } }); @@ -139,7 +139,7 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services { /// Trigger a browser download of `bytes` named after the last component of `path`. fn download(path: &str, bytes: &[u8]) -> Result<(), String> { let js = |e: wasm_bindgen::JsValue| format!("{e:?}"); - let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "slidecraft".into()); + let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "deckcraft".into()); let window = web_sys::window().ok_or("no window")?; let document = window.document().ok_or("no document")?; let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes)); @@ -170,7 +170,7 @@ fn mime_for(name: &str) -> &'static str { Some("pdf") => "application/pdf", Some("txt") => "text/plain", Some("pptx") => "application/vnd.openxmlformats-officedocument.presentationml.presentation", - Some("slidecraft") => "application/zip", + Some("deckcraft") => "application/zip", _ => "application/octet-stream", } } diff --git a/apps/slidecraft/Cargo.toml b/apps/deckcraft/Cargo.toml similarity index 70% rename from apps/slidecraft/Cargo.toml rename to apps/deckcraft/Cargo.toml index e675bc6..fb49562 100644 --- a/apps/slidecraft/Cargo.toml +++ b/apps/deckcraft/Cargo.toml @@ -1,26 +1,26 @@ [package] -name = "slidecraft" +name = "deckcraft" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft desktop app" +description = "DeckCraft desktop app" publish = false -default-run = "slidecraft" +default-run = "deckcraft" [lints] workspace = true [dependencies] -slidecraft-engine = { workspace = true } -slidecraft-ui-egui = { workspace = true } -slidecraft-media = { workspace = true } +deckcraft-engine = { workspace = true } +deckcraft-ui-egui = { workspace = true } +deckcraft-media = { workspace = true } eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] } egui = { workspace = true } serde_json = { workspace = true } log = { workspace = true } [build-dependencies] -# Icon + VERSIONINFO for slidecraft.exe; build.rs only uses it when the target is Windows. +# Icon + VERSIONINFO for deckcraft.exe; build.rs only uses it when the target is Windows. winresource = "0.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/apps/deckcraft/build.rs b/apps/deckcraft/build.rs new file mode 100644 index 0000000..5bb30d5 --- /dev/null +++ b/apps/deckcraft/build.rs @@ -0,0 +1,30 @@ +//! Windows only: embed the app icon and version info (VERSIONINFO) into `deckcraft.exe`, so it +//! shows in Explorer, the taskbar, the Start menu and Alt-Tab. +//! +//! On every other target this does nothing. A missing resource compiler is a warning, so a +//! cross-compile from macOS or Linux still links, unless `DECKCRAFT_REQUIRE_WINRES=1` turns it +//! into an error (for release builds). + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../../assets/app-icon/deckcraft.ico"); + println!("cargo:rerun-if-env-changed=DECKCRAFT_REQUIRE_WINRES"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { + return; + } + let mut res = winresource::WindowsResource::new(); + res.set_icon("../../assets/app-icon/deckcraft.ico") + .set("ProductName", "DeckCraft") + .set("FileDescription", "DeckCraft presentations") + .set("CompanyName", "Learning Machines LLC") + .set("LegalCopyright", "Copyright (c) the DeckCraft authors. MIT OR Apache-2.0.") + .set("OriginalFilename", "deckcraft.exe") + .set("InternalName", "deckcraft"); + if let Err(e) = res.compile() { + if std::env::var_os("DECKCRAFT_REQUIRE_WINRES").is_some() { + eprintln!("embedding Windows resources failed: {e}"); + std::process::exit(1); + } + println!("cargo:warning=deckcraft.exe built without icon/version resources: {e}"); + } +} diff --git a/apps/slidecraft/src/audio.rs b/apps/deckcraft/src/audio.rs similarity index 93% rename from apps/slidecraft/src/audio.rs rename to apps/deckcraft/src/audio.rs index cbe9977..86682ad 100644 --- a/apps/slidecraft/src/audio.rs +++ b/apps/deckcraft/src/audio.rs @@ -1,8 +1,8 @@ //! cpal audio output for media playback: the device pulls mixed samples from the -//! `slidecraft_media::Player`, which makes it the playback clock video frames follow. +//! `deckcraft_media::Player`, which makes it the playback clock video frames follow. use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use slidecraft_media::AudioOut; +use deckcraft_media::AudioOut; #[derive(Default)] pub struct CpalOut { diff --git a/apps/slidecraft/src/control_server.rs b/apps/deckcraft/src/control_server.rs similarity index 87% rename from apps/slidecraft/src/control_server.rs rename to apps/deckcraft/src/control_server.rs index 17a110a..c2ad876 100644 --- a/apps/slidecraft/src/control_server.rs +++ b/apps/deckcraft/src/control_server.rs @@ -1,24 +1,24 @@ //! Loopback JSON-lines control server: one request per line, one reply per line. -//! This is the transport the MCP server (`slidecraft-cli mcp --connect`) wraps. +//! This is the transport the MCP server (`deckcraft-cli mcp --connect`) wraps. use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::mpsc::{Receiver, Sender, channel}; use std::time::Duration; +use deckcraft_ui_egui::ControlRequest; use serde_json::{Value, json}; -use slidecraft_ui_egui::ControlRequest; pub fn start(port: u16, ctx: egui::Context) -> Receiver { let (tx, rx) = channel::(); let listener = match TcpListener::bind(("127.0.0.1", port)) { Ok(l) => l, Err(e) => { - eprintln!("slidecraft: control server failed to bind 127.0.0.1:{port}: {e}"); + eprintln!("deckcraft: control server failed to bind 127.0.0.1:{port}: {e}"); return rx; } }; - eprintln!("slidecraft: control server listening on 127.0.0.1:{port}"); + eprintln!("deckcraft: control server listening on 127.0.0.1:{port}"); std::thread::spawn(move || { for stream in listener.incoming().flatten() { let tx = tx.clone(); diff --git a/apps/slidecraft/src/main.rs b/apps/deckcraft/src/main.rs similarity index 78% rename from apps/slidecraft/src/main.rs rename to apps/deckcraft/src/main.rs index 6627b18..d9b5587 100644 --- a/apps/slidecraft/src/main.rs +++ b/apps/deckcraft/src/main.rs @@ -1,10 +1,10 @@ -//! SlideCraft desktop app. +//! DeckCraft desktop app. //! -//! Usage: `slidecraft [--control ] [--sample] [--show] [files…]` +//! Usage: `deckcraft [--control ] [--sample] [--show] [files…]` //! -//! `--control ` (or `SLIDECRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server: +//! `--control ` (or `DECKCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server: //! `{"id":1,"method":"ui.inspect","params":{}}` → `{"id":1,"ok":true,"result":…}`. -//! See `slidecraft_ui_egui::control` for the methods. +//! See `deckcraft_ui_egui::control` for the methods. #![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] @@ -12,8 +12,8 @@ mod audio; mod control_server; -use slidecraft_engine::Session; -use slidecraft_ui_egui::{Services, SlideApp}; +use deckcraft_engine::Session; +use deckcraft_ui_egui::{Services, SlideApp}; struct App(SlideApp); @@ -34,38 +34,38 @@ impl eframe::App for App { fn prefs_path() -> Option { let base = if cfg!(target_os = "macos") { - std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/SlideCraft")) + std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/DeckCraft")) } else if cfg!(windows) { - std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("SlideCraft")) + std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("DeckCraft")) } else { std::env::var_os("XDG_CONFIG_HOME") .map(std::path::PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))) - .map(|c| c.join("slidecraft")) + .map(|c| c.join("deckcraft")) }; base.map(|b| b.join("ui.json")) } fn load_prefs(app: &mut SlideApp) { - if std::env::var_os("SLIDECRAFT_NO_PREFS").is_some() { + if std::env::var_os("DECKCRAFT_NO_PREFS").is_some() { return; } if let Some(p) = prefs_path() && let Ok(bytes) = std::fs::read(&p) - && let Ok(ui) = serde_json::from_slice::(&bytes) + && let Ok(ui) = serde_json::from_slice::(&bytes) { app.ui = ui; } if let Some(p) = prefs_path().map(|p| p.with_file_name("prefs.json")) && let Ok(bytes) = std::fs::read(&p) - && let Ok(prefs) = serde_json::from_slice::(&bytes) + && let Ok(prefs) = serde_json::from_slice::(&bytes) { app.session.prefs = prefs; } } fn save_prefs(app: &SlideApp) { - if std::env::var_os("SLIDECRAFT_NO_PREFS").is_some() { + if std::env::var_os("DECKCRAFT_NO_PREFS").is_some() { return; } if let Some(p) = prefs_path() { @@ -91,8 +91,8 @@ fn services() -> Services { ), "video" => d.add_filter("Video", &["mp4", "m4v", "mov", "webm", "mkv", "wmv"]), _ => d - .add_filter("Presentations", &["slidecraft", "pptx", "potx", "ppsx"]) - .add_filter("SlideCraft", &["slidecraft"]) + .add_filter("Presentations", &["deckcraft", "pptx", "potx", "ppsx"]) + .add_filter("DeckCraft", &["deckcraft"]) .add_filter("PowerPoint", &["pptx", "potx", "ppsx"]) .add_filter("Outline", &["txt", "md"]), }; @@ -109,23 +109,23 @@ fn services() -> Services { rgba.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?; Some(out) })), - // Media is mixed and clocked by the output device; `SLIDECRAFT_NO_AUDIO` plays silently. - audio_out: std::env::var_os("SLIDECRAFT_NO_AUDIO") + // Media is mixed and clocked by the output device; `DECKCRAFT_NO_AUDIO` plays silently. + audio_out: std::env::var_os("DECKCRAFT_NO_AUDIO") .is_none() - .then(|| Box::new(audio::CpalOut::default()) as Box), + .then(|| Box::new(audio::CpalOut::default()) as Box), ..Default::default() } } -const APP_ID: &str = "ai.storyteller.slidecraft"; +const APP_ID: &str = "ai.storyteller.deckcraft"; fn app_icon() -> Option { - let png: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.slidecraft.png"); + let png: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.deckcraft.png"); eframe::icon_data::from_png_bytes(png).map_err(|e| log::warn!("app icon: {e}")).ok() } fn main() -> eframe::Result { - let mut control_port: Option = std::env::var("SLIDECRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); + let mut control_port: Option = std::env::var("DECKCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); let mut files = Vec::new(); let mut sample = false; let mut show = false; @@ -136,7 +136,7 @@ fn main() -> eframe::Result { "--sample" => sample = true, "--show" => show = true, "--version" => { - println!("slidecraft {}", env!("CARGO_PKG_VERSION")); + println!("deckcraft {}", env!("CARGO_PKG_VERSION")); return Ok(()); } _ => files.push(a), @@ -144,7 +144,7 @@ fn main() -> eframe::Result { } let mut options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_title("SlideCraft") + .with_title("DeckCraft") .with_inner_size([1440.0, 900.0]) .with_min_inner_size([900.0, 560.0]) .with_drag_and_drop(true) @@ -158,21 +158,21 @@ fn main() -> eframe::Result { options.viewport = options.viewport.with_icon(icon); } #[cfg(all(unix, not(target_os = "macos")))] - if std::env::var_os("DISPLAY").is_some() && std::env::var_os("SLIDECRAFT_WAYLAND").is_none() { + if std::env::var_os("DISPLAY").is_some() && std::env::var_os("DECKCRAFT_WAYLAND").is_none() { options.event_loop_builder = Some(Box::new(|b| { use winit::platform::x11::EventLoopBuilderExtX11; b.with_x11(); })); } eframe::run_native( - "SlideCraft", + "DeckCraft", options, Box::new(move |cc| { let mut app = SlideApp::new(Session::new(), services()); load_prefs(&mut app); // AutoRecover: reopen what a previous run left unsaved, then keep it current. - if std::env::var_os("SLIDECRAFT_NO_RECOVERY").is_none() { - app.session.recovery_dir = slidecraft_engine::recovery::default_dir(); + if std::env::var_os("DECKCRAFT_NO_RECOVERY").is_none() { + app.session.recovery_dir = deckcraft_engine::recovery::default_dir(); if let Ok(v) = app.session.execute("file.recovery.open", &serde_json::json!({})) && let Some(n) = v.get("opened").and_then(|o| o.as_array()).map(Vec::len).filter(|n| *n > 0) { @@ -184,12 +184,12 @@ fn main() -> eframe::Result { let rx = control_server::start(port, cc.egui_ctx.clone()); app = app.with_control(rx); } - if sample && let Err(e) = slidecraft_engine::sample::open_sample(&mut app.session) { - eprintln!("slidecraft: sample: {e}"); + if sample && let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) { + eprintln!("deckcraft: sample: {e}"); } for f in files { if let Err(e) = app.open_path(&f) { - eprintln!("slidecraft: {f}: {e}"); + eprintln!("deckcraft: {f}: {e}"); } } if show { diff --git a/apps/slidecraft-cli/Cargo.toml b/apps/slidecraft-cli/Cargo.toml deleted file mode 100644 index b1f9d36..0000000 --- a/apps/slidecraft-cli/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "slidecraft-cli" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "SlideCraft on the command line: render, convert, run commands, inspect, and serve MCP" -publish = false - -[lints] -workspace = true - -[[bin]] -name = "slidecraft-cli" -path = "src/main.rs" - -[dependencies] -slidecraft-engine = { workspace = true } -slidecraft-mcp = { workspace = true } -serde_json = { workspace = true } diff --git a/apps/slidecraft-web/index.html b/apps/slidecraft-web/index.html deleted file mode 100644 index 4f2d54d..0000000 --- a/apps/slidecraft-web/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - SlideCraft - - - - - - -
Loading SlideCraft…
- - diff --git a/apps/slidecraft/build.rs b/apps/slidecraft/build.rs deleted file mode 100644 index 40c647a..0000000 --- a/apps/slidecraft/build.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Windows only: embed the app icon and version info (VERSIONINFO) into `slidecraft.exe`, so it -//! shows in Explorer, the taskbar, the Start menu and Alt-Tab. -//! -//! On every other target this does nothing. A missing resource compiler is a warning, so a -//! cross-compile from macOS or Linux still links, unless `SLIDECRAFT_REQUIRE_WINRES=1` turns it -//! into an error (for release builds). - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=../../assets/app-icon/slidecraft.ico"); - println!("cargo:rerun-if-env-changed=SLIDECRAFT_REQUIRE_WINRES"); - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { - return; - } - let mut res = winresource::WindowsResource::new(); - res.set_icon("../../assets/app-icon/slidecraft.ico") - .set("ProductName", "SlideCraft") - .set("FileDescription", "SlideCraft presentations") - .set("CompanyName", "Learning Machines LLC") - .set("LegalCopyright", "Copyright (c) the SlideCraft authors. MIT OR Apache-2.0.") - .set("OriginalFilename", "slidecraft.exe") - .set("InternalName", "slidecraft"); - if let Err(e) = res.compile() { - if std::env::var_os("SLIDECRAFT_REQUIRE_WINRES").is_some() { - eprintln!("embedding Windows resources failed: {e}"); - std::process::exit(1); - } - println!("cargo:warning=slidecraft.exe built without icon/version resources: {e}"); - } -} diff --git a/assets/app-icon/slidecraft-1024.png b/assets/app-icon/deckcraft-1024.png similarity index 100% rename from assets/app-icon/slidecraft-1024.png rename to assets/app-icon/deckcraft-1024.png diff --git a/assets/app-icon/slidecraft.icns b/assets/app-icon/deckcraft.icns similarity index 100% rename from assets/app-icon/slidecraft.icns rename to assets/app-icon/deckcraft.icns diff --git a/assets/app-icon/slidecraft.ico b/assets/app-icon/deckcraft.ico similarity index 100% rename from assets/app-icon/slidecraft.ico rename to assets/app-icon/deckcraft.ico diff --git a/assets/app-icon/hicolor/128x128/apps/ai.storyteller.slidecraft.png b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.deckcraft.png similarity index 100% rename from assets/app-icon/hicolor/128x128/apps/ai.storyteller.slidecraft.png rename to assets/app-icon/hicolor/128x128/apps/ai.storyteller.deckcraft.png diff --git a/assets/app-icon/hicolor/16x16/apps/ai.storyteller.slidecraft.png b/assets/app-icon/hicolor/16x16/apps/ai.storyteller.deckcraft.png similarity index 100% rename from assets/app-icon/hicolor/16x16/apps/ai.storyteller.slidecraft.png rename to assets/app-icon/hicolor/16x16/apps/ai.storyteller.deckcraft.png diff --git a/assets/app-icon/hicolor/24x24/apps/ai.storyteller.slidecraft.png b/assets/app-icon/hicolor/24x24/apps/ai.storyteller.deckcraft.png similarity index 100% rename from assets/app-icon/hicolor/24x24/apps/ai.storyteller.slidecraft.png rename to assets/app-icon/hicolor/24x24/apps/ai.storyteller.deckcraft.png diff --git a/assets/app-icon/hicolor/256x256/apps/ai.storyteller.slidecraft.png b/assets/app-icon/hicolor/256x256/apps/ai.storyteller.deckcraft.png similarity index 100% rename from assets/app-icon/hicolor/256x256/apps/ai.storyteller.slidecraft.png rename to assets/app-icon/hicolor/256x256/apps/ai.storyteller.deckcraft.png diff --git a/assets/app-icon/hicolor/32x32/apps/ai.storyteller.slidecraft.png b/assets/app-icon/hicolor/32x32/apps/ai.storyteller.deckcraft.png similarity index 100% rename from assets/app-icon/hicolor/32x32/apps/ai.storyteller.slidecraft.png rename to assets/app-icon/hicolor/32x32/apps/ai.storyteller.deckcraft.png diff --git a/assets/app-icon/hicolor/48x48/apps/ai.storyteller.slidecraft.png b/assets/app-icon/hicolor/48x48/apps/ai.storyteller.deckcraft.png similarity index 100% rename from assets/app-icon/hicolor/48x48/apps/ai.storyteller.slidecraft.png rename to assets/app-icon/hicolor/48x48/apps/ai.storyteller.deckcraft.png diff --git a/assets/app-icon/hicolor/512x512/apps/ai.storyteller.slidecraft.png b/assets/app-icon/hicolor/512x512/apps/ai.storyteller.deckcraft.png similarity index 100% rename from assets/app-icon/hicolor/512x512/apps/ai.storyteller.slidecraft.png rename to assets/app-icon/hicolor/512x512/apps/ai.storyteller.deckcraft.png diff --git a/assets/app-icon/hicolor/64x64/apps/ai.storyteller.slidecraft.png b/assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png similarity index 100% rename from assets/app-icon/hicolor/64x64/apps/ai.storyteller.slidecraft.png rename to assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png diff --git a/assets/app-icon/icon-source.slidecraft b/assets/app-icon/icon-source.deckcraft similarity index 100% rename from assets/app-icon/icon-source.slidecraft rename to assets/app-icon/icon-source.deckcraft diff --git a/crates/anim/Cargo.toml b/crates/anim/Cargo.toml index 53c2805..fd9c9ec 100644 --- a/crates/anim/Cargo.toml +++ b/crates/anim/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "slidecraft-anim" +name = "deckcraft-anim" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft slide show timing: animation sequences, per-shape animation state over time, and transition frame layouts" +description = "DeckCraft slide show timing: animation sequences, per-shape animation state over time, and transition frame layouts" [lints] workspace = true [dependencies] -slidecraft-model = { workspace = true } -slidecraft-geom = { workspace = true } +deckcraft-model = { workspace = true } +deckcraft-geom = { workspace = true } serde = { workspace = true } diff --git a/crates/anim/src/effects.rs b/crates/anim/src/effects.rs index a795635..37776f8 100644 --- a/crates/anim/src/effects.rs +++ b/crates/anim/src/effects.rs @@ -2,7 +2,7 @@ use std::f64::consts::PI; -use slidecraft_model::{AnimClass, Rgba, Xfrm}; +use deckcraft_model::{AnimClass, Rgba, Xfrm}; use crate::AnimState; use crate::easing::{bounce_out, ease, smooth}; diff --git a/crates/anim/src/lib.rs b/crates/anim/src/lib.rs index a5a8744..5452ff1 100644 --- a/crates/anim/src/lib.rs +++ b/crates/anim/src/lib.rs @@ -1,4 +1,4 @@ -//! Slide show timing for SlideCraft. +//! Slide show timing for DeckCraft. //! //! - [`Timeline`]: a slide's animation list grouped into click steps, with per-shape (and //! per-paragraph) [`AnimState`] at any point of the show. @@ -22,8 +22,8 @@ mod transition; #[cfg(test)] mod tests; +pub use deckcraft_model::Rgba; use serde::{Deserialize, Serialize}; -pub use slidecraft_model::Rgba; pub use easing::ease; pub use morph::{morph_pairs, morph_xfrm}; diff --git a/crates/anim/src/morph.rs b/crates/anim/src/morph.rs index 6689153..ddbde9c 100644 --- a/crates/anim/src/morph.rs +++ b/crates/anim/src/morph.rs @@ -1,6 +1,6 @@ //! Morph: matching shapes between two slides and interpolating their boxes. -use slidecraft_model::{Shape, ShapeId, Slide, Xfrm}; +use deckcraft_model::{Shape, ShapeId, Slide, Xfrm}; use crate::clampf; diff --git a/crates/anim/src/show.rs b/crates/anim/src/show.rs index efd236d..a964e87 100644 --- a/crates/anim/src/show.rs +++ b/crates/anim/src/show.rs @@ -1,7 +1,7 @@ //! Slide show navigation: click steps, slide order (hidden slides, custom shows, ranges), loop //! and automatic advance. -use slidecraft_model::Presentation; +use deckcraft_model::Presentation; use crate::Timeline; diff --git a/crates/anim/src/tests.rs b/crates/anim/src/tests.rs index 9337d91..4a7b369 100644 --- a/crates/anim/src/tests.rs +++ b/crates/anim/src/tests.rs @@ -2,8 +2,8 @@ use std::sync::Arc; -use slidecraft_model::anim::{ANIMATIONS, TRANSITIONS, TextBuild}; -use slidecraft_model::{ +use deckcraft_model::anim::{ANIMATIONS, TRANSITIONS, TextBuild}; +use deckcraft_model::{ AnimClass, AnimStart, Animation, ColorRef, CustomShow, Presentation, Rgba, Shape, ShapeId, Slide, SlideId, TextBody, Transition, Xfrm, }; diff --git a/crates/anim/src/timeline.rs b/crates/anim/src/timeline.rs index 9a60d13..e73acfd 100644 --- a/crates/anim/src/timeline.rs +++ b/crates/anim/src/timeline.rs @@ -1,8 +1,8 @@ //! A slide's animation sequence: click steps, effect start times and per-shape state over time. -use slidecraft_model::anim::TextBuild; -use slidecraft_model::style::Fill; -use slidecraft_model::{AnimClass, AnimStart, Animation, ColorScheme, Rgba, SchemeSlot, ShapeId, Slide, Xfrm}; +use deckcraft_model::anim::TextBuild; +use deckcraft_model::style::Fill; +use deckcraft_model::{AnimClass, AnimStart, Animation, ColorScheme, Rgba, SchemeSlot, ShapeId, Slide, Xfrm}; use crate::AnimState; use crate::effects::{EffectSpec, exit_base, motion_path}; @@ -116,7 +116,7 @@ fn sanitize_xfrm(x: Xfrm, sw: f64, sh: f64) -> Xfrm { } fn default_scheme() -> Option { - slidecraft_model::theme::builtin_color_schemes().into_iter().next() + deckcraft_model::theme::builtin_color_schemes().into_iter().next() } impl Timeline { @@ -139,11 +139,11 @@ impl Timeline { owned.as_ref() } }; - let resolve = |c: &slidecraft_model::ColorRef| -> Rgba { + let resolve = |c: &deckcraft_model::ColorRef| -> Rgba { match scheme { Some(s) => c.resolve(s, None), None => match &c.base { - slidecraft_model::ColorBase::Rgb { rgb } => *rgb, + deckcraft_model::ColorBase::Rgb { rgb } => *rgb, _ => Rgba::rgb(0x15, 0x60, 0x82), }, } @@ -180,7 +180,7 @@ impl Timeline { (None, TextBuild::ByParagraph) => { let mut v: Vec> = vec![]; if let Some(t) = shape.and_then(|s| s.text.as_ref()) { - let filled = |p: &slidecraft_model::text::Paragraph| p.runs.iter().any(|r| !r.text.trim().is_empty()); + let filled = |p: &deckcraft_model::text::Paragraph| p.runs.iter().any(|r| !r.text.trim().is_empty()); let top = t.paragraphs.iter().filter(|p| filled(p)).map(|p| p.level).min().unwrap_or(0); let mut owner: Option = None; for (i, p) in t.paragraphs.iter().enumerate() { @@ -465,7 +465,7 @@ fn status(e: &EffectTiming, step: usize, t: f64) -> Status { Status::Active(p.clamp(0.0, 1.0)) } -fn build_spec(a: &Animation, xf: Xfrm, sw: f64, sh: f64, resolve: &dyn Fn(&slidecraft_model::ColorRef) -> Rgba) -> EffectSpec { +fn build_spec(a: &Animation, xf: Xfrm, sw: f64, sh: f64, resolve: &dyn Fn(&deckcraft_model::ColorRef) -> Rgba) -> EffectSpec { let effect = if a.class == AnimClass::Exit { exit_base(&a.effect).to_string() } else { a.effect.clone() }; let path = if a.class == AnimClass::Path { Some(motion_path(&a.effect, &a.option, a.path.as_deref())) } else { None }; EffectSpec { diff --git a/crates/anim/src/transition.rs b/crates/anim/src/transition.rs index 008b906..876c88f 100644 --- a/crates/anim/src/transition.rs +++ b/crates/anim/src/transition.rs @@ -196,7 +196,7 @@ fn scale_rect(r: R, s: f64) -> R { // ---------- effects ---------- -/// Layers of the transition `kind` (a [`slidecraft_model::anim::TRANSITIONS`] id) with `option` at +/// Layers of the transition `kind` (a [`deckcraft_model::anim::TRANSITIONS`] id) with `option` at /// progress `t` (0..1). Draw in order over black. At `t >= 1` the result is the new slide alone; /// at `t <= 0` (or NaN) the old slide alone. Morph returns a plain crossfade (the UI renders morph /// frames with [`crate::morph_pairs`]). @@ -736,7 +736,7 @@ fn layers(kind: &str, option: &str, t: f64, depth: u32) -> Vec { "random" if depth == 0 => { let h = option.bytes().fold(0x51ED_u64, |h, b| hash64(h ^ b as u64)); let k = RANDOM_KINDS.get((h % RANDOM_KINDS.len() as u64) as usize).copied().unwrap_or("fade"); - let opt = slidecraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == k).and_then(|x| x.4.first().copied()).unwrap_or(""); + let opt = deckcraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == k).and_then(|x| x.4.first().copied()).unwrap_or(""); layers(k, opt, t, depth + 1) } _ => crossfade(t), diff --git a/crates/av1/Cargo.toml b/crates/av1/Cargo.toml index c08e31d..ddd1c56 100644 --- a/crates/av1/Cargo.toml +++ b/crates/av1/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-av1" +name = "deckcraft-av1" description = "Clean-room AV1 decoder (AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1)" version.workspace = true edition.workspace = true @@ -14,6 +14,6 @@ default = ["threads"] threads = ["dep:rayon"] [dependencies] -slidecraft-bitstream = { workspace = true } +deckcraft-bitstream = { workspace = true } thiserror = { workspace = true } rayon = { workspace = true, optional = true } diff --git a/crates/bitstream/Cargo.toml b/crates/bitstream/Cargo.toml index 99cb98f..8465f1c 100644 --- a/crates/bitstream/Cargo.toml +++ b/crates/bitstream/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-bitstream" +name = "deckcraft-bitstream" version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/bitstream/src/lib.rs b/crates/bitstream/src/lib.rs index e5df0bb..b5acf03 100644 --- a/crates/bitstream/src/lib.rs +++ b/crates/bitstream/src/lib.rs @@ -1,4 +1,4 @@ -//! Bit-level reading and writing shared by SlideCraft's codec crates (ported from FilmCraft). +//! Bit-level reading and writing shared by DeckCraft's codec crates (ported from FilmCraft). //! //! - [`BitReader`]: MSB-first reader with Exp-Golomb (`ue(v)`, `se(v)`), as used by H.264/HEVC/AV1 headers. //! - [`BitWriter`]: the matching writer (for encoders and header rewriting). diff --git a/crates/color/Cargo.toml b/crates/color/Cargo.toml index f59d39d..9d5a552 100644 --- a/crates/color/Cargo.toml +++ b/crates/color/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "slidecraft-color" +name = "deckcraft-color" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft colour: RGB/HSL, theme colour slots, DrawingML-style colour transforms" +description = "DeckCraft colour: RGB/HSL, theme colour slots, DrawingML-style colour transforms" [lints] workspace = true diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs index b6a318f..ba50e9a 100644 --- a/crates/color/src/lib.rs +++ b/crates/color/src/lib.rs @@ -1,4 +1,4 @@ -//! SlideCraft colour: sRGB colours, theme colour slots and DrawingML-style colour transforms +//! DeckCraft colour: sRGB colours, theme colour slots and DrawingML-style colour transforms //! (tint, shade, luminance modulation/offset, alpha…), so theme-relative colours resolve the way //! presentation files expect. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index eaedebd..1f4dbfc 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -1,24 +1,24 @@ [package] -name = "slidecraft-engine" +name = "deckcraft-engine" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft engine: sessions, undo history, selection, the command registry every frontend dispatches through, and pointer tools" +description = "DeckCraft engine: sessions, undo history, selection, the command registry every frontend dispatches through, and pointer tools" [lints] workspace = true [dependencies] -slidecraft-geom = { workspace = true } -slidecraft-color = { workspace = true } -slidecraft-model = { workspace = true } -slidecraft-fonts = { workspace = true } -slidecraft-text = { workspace = true } -slidecraft-render = { workspace = true } -slidecraft-pdf = { workspace = true } -slidecraft-format = { workspace = true } -slidecraft-pptx = { workspace = true } -slidecraft-media = { workspace = true } +deckcraft-geom = { workspace = true } +deckcraft-color = { workspace = true } +deckcraft-model = { workspace = true } +deckcraft-fonts = { workspace = true } +deckcraft-text = { workspace = true } +deckcraft-render = { workspace = true } +deckcraft-pdf = { workspace = true } +deckcraft-format = { workspace = true } +deckcraft-pptx = { workspace = true } +deckcraft-media = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } i_overlay = { workspace = true } diff --git a/crates/engine/src/cmd/animation.rs b/crates/engine/src/cmd/animation.rs index c19f92c..e6844f5 100644 --- a/crates/engine/src/cmd/animation.rs +++ b/crates/engine/src/cmd/animation.rs @@ -2,8 +2,8 @@ use std::sync::Arc; +use deckcraft_model::anim::{ANIMATIONS, AnimClass, AnimStart, Animation, TextBuild, animation_info}; use serde_json::{Value, json}; -use slidecraft_model::anim::{ANIMATIONS, AnimClass, AnimStart, Animation, TextBuild, animation_info}; use super::*; use crate::{Result, Session}; @@ -227,7 +227,7 @@ fn timing(s: &mut Session, p: &Value) -> Result { let delay = p.get("delay").and_then(Value::as_u64).map(|v| v.min(600_000) as u32); let repeat = p.get("repeat").and_then(Value::as_u64).map(|v| v.min(u32::MAX as u64) as u32); let rewind = bool_param(p, "rewind"); - let trigger = p.get("trigger").map(|v| v.as_u64().and_then(|x| u32::try_from(x).ok()).map(slidecraft_model::ShapeId)); + let trigger = p.get("trigger").map(|v| v.as_u64().and_then(|x| u32::try_from(x).ok()).map(deckcraft_model::ShapeId)); each_target(s, p, |a| { if let Some(x) = start { a.start = x; diff --git a/crates/engine/src/cmd/arrange.rs b/crates/engine/src/cmd/arrange.rs index 49ee64b..c755050 100644 --- a/crates/engine/src/cmd/arrange.rs +++ b/crates/engine/src/cmd/arrange.rs @@ -1,8 +1,8 @@ //! Arrange: order, group, align, distribute, rotate and flip. +use deckcraft_geom::{Point, Rect, Xfrm}; +use deckcraft_model::{Shape, ShapeId, ShapeKind}; use serde_json::{Value, json}; -use slidecraft_geom::{Point, Rect, Xfrm}; -use slidecraft_model::{Shape, ShapeId, ShapeKind}; use super::*; use crate::{Result, Session}; @@ -145,7 +145,7 @@ fn group(s: &mut Session, p: &Value) -> Result { pub(crate) fn child_to_parent(group: &Xfrm, ch: &Xfrm, c: &Xfrm) -> Xfrm { let sx = if ch.w.abs() > 1e-9 { group.w / ch.w } else { 1.0 }; let sy = if ch.h.abs() > 1e-9 { group.h / ch.h } else { 1.0 }; - let a = slidecraft_geom::group_child_affine(group, Rect::new(ch.x, ch.y, ch.x + ch.w, ch.y + ch.h)); + let a = deckcraft_geom::group_child_affine(group, Rect::new(ch.x, ch.y, ch.x + ch.w, ch.y + ch.h)); let center = a * Point::new(c.x + c.w / 2.0, c.y + c.h / 2.0); let (w, h) = (c.w * sx.abs(), c.h * sy.abs()); let mut rot = c.rot + group.rot; diff --git a/crates/engine/src/cmd/design.rs b/crates/engine/src/cmd/design.rs index 6cf6b03..a8fd9a3 100644 --- a/crates/engine/src/cmd/design.rs +++ b/crates/engine/src/cmd/design.rs @@ -3,11 +3,11 @@ use std::sync::Arc; +use deckcraft_geom::Size; +use deckcraft_model::text::{Run, RunKind, TextBody}; +use deckcraft_model::theme::{builtin_color_schemes, builtin_font_schemes, builtin_themes}; +use deckcraft_model::{Background, Fill, PhType, Presentation, Shape, ShapeId, defaults}; use serde_json::{Value, json}; -use slidecraft_geom::Size; -use slidecraft_model::text::{Run, RunKind, TextBody}; -use slidecraft_model::theme::{builtin_color_schemes, builtin_font_schemes, builtin_themes}; -use slidecraft_model::{Background, Fill, PhType, Presentation, Shape, ShapeId, defaults}; use super::*; use crate::{Result, Session, Target}; @@ -114,7 +114,7 @@ fn colors(s: &mut Session, p: &Value) -> Result { } if let Some(c) = &custom { for (k, v) in c { - if let (Some(slot), Some(rgb)) = (slidecraft_color::SchemeSlot::from_xml(k), v.as_str().and_then(slidecraft_color::Rgba::from_hex)) { + if let (Some(slot), Some(rgb)) = (deckcraft_color::SchemeSlot::from_xml(k), v.as_str().and_then(deckcraft_color::Rgba::from_hex)) { m.theme.colors.set(slot, rgb); } } @@ -134,10 +134,10 @@ fn fonts(s: &mut Session, p: &Value) -> Result { } else { let major = str_param(p, "major").ok_or_else(|| bad("design.fonts", "give `name`, or `major` and `minor`"))?; let minor = str_param(p, "minor").unwrap_or(major); - slidecraft_model::theme::FontScheme { + deckcraft_model::theme::FontScheme { name: "Custom".into(), - major: slidecraft_model::theme::FontSet { latin: major.into(), ..Default::default() }, - minor: slidecraft_model::theme::FontSet { latin: minor.into(), ..Default::default() }, + major: deckcraft_model::theme::FontSet { latin: major.into(), ..Default::default() }, + minor: deckcraft_model::theme::FontSet { latin: minor.into(), ..Default::default() }, } }; s.edit(|doc, sel| { @@ -155,16 +155,12 @@ fn background(s: &mut Session, p: &Value) -> Result { Some(Background::Fill { fill: Fill::solid(c) }) } else if let Some(i) = usize_param(p, "style") { // Background styles: 3 theme bg fills × 4 colours (bg1, bg2, tx1, tx2). - let slots = [ - slidecraft_color::SchemeSlot::Bg1, - slidecraft_color::SchemeSlot::Bg2, - slidecraft_color::SchemeSlot::Tx2, - slidecraft_color::SchemeSlot::Tx1, - ]; + let slots = + [deckcraft_color::SchemeSlot::Bg1, deckcraft_color::SchemeSlot::Bg2, deckcraft_color::SchemeSlot::Tx2, deckcraft_color::SchemeSlot::Tx1]; let i = i.clamp(1, 12) - 1; Some(Background::Ref { idx: 1001 + (i / 4) as u32, - color: slidecraft_model::ColorRef::scheme(slots.get(i % 4).copied().unwrap_or(slidecraft_color::SchemeSlot::Bg1)), + color: deckcraft_model::ColorRef::scheme(slots.get(i % 4).copied().unwrap_or(deckcraft_color::SchemeSlot::Bg1)), }) } else if let Some(g) = p.get("gradient") { let mut g = super::shape::gradient_param(g).ok_or_else(|| bad("design.background", "gradient needs two or more `stops`"))?; @@ -172,17 +168,17 @@ fn background(s: &mut Session, p: &Value) -> Result { Some(Background::Fill { fill: Fill::Gradient(g) }) } else if let Some(pt) = p.get("pattern") { Some(Background::Fill { - fill: Fill::Pattern(slidecraft_model::style::PatternFill { + fill: Fill::Pattern(deckcraft_model::style::PatternFill { preset: pt.get("preset").and_then(Value::as_str).unwrap_or("pct50").to_string(), - fg: pt.get("fg").and_then(color_value).unwrap_or(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Accent1)), - bg: pt.get("bg").and_then(color_value).unwrap_or(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Bg1)), + fg: pt.get("fg").and_then(color_value).unwrap_or(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Accent1)), + bg: pt.get("bg").and_then(color_value).unwrap_or(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1)), }), }) } else if p.get("picture").is_some() { let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "design.background")?; let ct = super::insert::content_type(&name, &bytes); let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?; - Some(Background::Fill { fill: Fill::Picture(slidecraft_model::style::PictureFill { media, ..Default::default() }) }) + Some(Background::Fill { fill: Fill::Picture(deckcraft_model::style::PictureFill { media, ..Default::default() }) }) } else { return Err(bad("design.background", "give `color`, `gradient`, `pattern`, `picture`, `style` or `reset`")); }; @@ -234,7 +230,7 @@ fn scale_shapes(shapes: &mut [Shape], sx: f64, sy: f64, k: f64, ox: f64, oy: f64 x.h *= sy; } if let Some(t) = sh.text.as_mut() { - slidecraft_model::edit::format_all(t, &|r| { + deckcraft_model::edit::format_all(t, &|r| { if let Some(sz) = r.size.as_mut() { *sz = (*sz * k).max(1.0); } @@ -316,7 +312,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result { for i in list { let Some(layout) = doc.slides.get(i).map(|x| x.layout) else { continue }; let lay = doc.layout(layout).map(|(_, l)| l.clone()); - let is_title = lay.as_ref().is_some_and(|l| l.kind == slidecraft_model::LayoutType::Title); + let is_title = lay.as_ref().is_some_and(|l| l.kind == deckcraft_model::LayoutType::Title); let want = |k: PhType| { !(hf.hide_on_title && is_title) && match k { @@ -343,7 +339,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result { let Some(lp) = lp else { continue }; let body = match k { PhType::SlideNum => TextBody { - paragraphs: vec![slidecraft_model::Paragraph { + paragraphs: vec![deckcraft_model::Paragraph { runs: vec![Run { text: "‹#›".into(), props: Default::default(), @@ -354,7 +350,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result { ..Default::default() }, PhType::Date if hf.date_text.is_empty() => TextBody { - paragraphs: vec![slidecraft_model::Paragraph { + paragraphs: vec![deckcraft_model::Paragraph { runs: vec![Run { text: today(), props: Default::default(), kind: RunKind::Field { field: "datetime1".into() } }], ..Default::default() }], @@ -444,7 +440,7 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result { let name = str_param(p, "name").unwrap_or("Custom Layout").to_string(); s.edit(|doc, sel| { let mi = master_index(doc, sel); - let id = slidecraft_model::LayoutId(doc.alloc_id()); + let id = deckcraft_model::LayoutId(doc.alloc_id()); let tid = ShapeId(doc.alloc_id()); let size = doc.slide_size; let m = Arc::make_mut(doc.masters.get_mut(mi).ok_or_else(|| bad("master.insertLayout", "no master"))?); @@ -460,10 +456,10 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result { }; m.layouts.insert( at.min(m.layouts.len()), - slidecraft_model::Layout { + deckcraft_model::Layout { id, name, - kind: slidecraft_model::LayoutType::Custom, + kind: deckcraft_model::LayoutType::Custom, shapes: title.into_iter().collect(), background: None, show_master_shapes: true, @@ -528,11 +524,11 @@ fn insert_placeholder(s: &mut Session, p: &Value) -> Result { return Err(bad("master.insertPlaceholder", "select a layout in Slide Master view")); } let size = s.doc()?.doc.slide_size; - let rect = rect_param(p, "rect").unwrap_or(slidecraft_geom::Xfrm::new(size.width * 0.25, size.height * 0.3, size.width * 0.5, size.height * 0.4)); + let rect = rect_param(p, "rect").unwrap_or(deckcraft_geom::Xfrm::new(size.width * 0.25, size.height * 0.3, size.width * 0.5, size.height * 0.4)); let idx = s.doc()?.shapes().iter().filter_map(|x| x.ph.as_ref().map(|p| p.idx)).max().unwrap_or(0) + 1; let shape = Shape { xfrm: Some(rect), - ph: Some(slidecraft_model::Placeholder { kind, idx, ..Default::default() }), + ph: Some(deckcraft_model::Placeholder { kind, idx, ..Default::default() }), text: Some(TextBody::default()), ..Default::default() }; diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 31666b5..dfcbec6 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -2,8 +2,8 @@ use std::sync::Arc; +use deckcraft_model::{MediaId, Shape, ShapeId, Slide, SlideId}; use serde_json::{Value, json}; -use slidecraft_model::{MediaId, Shape, ShapeId, Slide, SlideId}; use super::*; use crate::{HistoryEntry, Result, Session}; @@ -49,10 +49,10 @@ fn slides_scope(s: &Session, p: &Value) -> bool { } fn media_of_shapes(shapes: &[Shape], out: &mut Vec) { - slidecraft_model::walk(shapes, &mut |s, _| { + deckcraft_model::walk(shapes, &mut |s, _| { match &s.kind { - slidecraft_model::ShapeKind::Picture { fill } => out.push(fill.media), - slidecraft_model::ShapeKind::Media(m) => { + deckcraft_model::ShapeKind::Picture { fill } => out.push(fill.media), + deckcraft_model::ShapeKind::Media(m) => { out.push(m.media); if let Some(p) = m.poster { out.push(p); @@ -60,7 +60,7 @@ fn media_of_shapes(shapes: &[Shape], out: &mut Vec) { } _ => {} } - if let Some(slidecraft_model::Fill::Picture(pf)) = &s.fill { + if let Some(deckcraft_model::Fill::Picture(pf)) = &s.fill { out.push(pf.media); } }); @@ -73,8 +73,8 @@ fn copy_inner(s: &mut Session, p: &Value) -> Result { if t.is_range() { let body = super::text::body_of(st, t).cloned().unwrap_or_default(); let (a, b) = t.ordered(); - let plain = slidecraft_model::edit::text_range(&body, a, b); - let sliced = slidecraft_model::edit::slice(&body, a, b); + let plain = deckcraft_model::edit::text_range(&body, a, b); + let sliced = deckcraft_model::edit::slice(&body, a, b); s.clipboard = crate::Clipboard { text: Some(sliced), plain: plain.clone(), ..Default::default() }; return Ok(plain); } @@ -126,8 +126,8 @@ fn cut(s: &mut Session, p: &Value) -> Result { } /// Give pasted shapes fresh ids (groups included) and map connector ends. -pub(crate) fn reid(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape]) { - fn rec(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape], depth: usize) { +pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) { + fn rec(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape], depth: usize) { if depth > 64 { return; } @@ -141,7 +141,7 @@ pub(crate) fn reid(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape rec(doc, shapes, 0); } -fn bring_media(doc: &mut slidecraft_model::Presentation, media: &[slidecraft_model::MediaItem]) { +fn bring_media(doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) { for m in media { if doc.media(m.id).is_none_or(|x| x.data != m.data) { if doc.media(m.id).is_none() { @@ -319,7 +319,7 @@ pub(crate) fn remove_shapes(shapes: &mut Vec, ids: &[ShapeId]) -> usize { fn count(v: &[Shape]) -> usize { let mut n = 0; - slidecraft_model::walk(v, &mut |_, _| n += 1); + deckcraft_model::walk(v, &mut |_, _| n += 1); n } diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 8db1968..55aac8b 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -1,7 +1,7 @@ //! File menu: new, open, save, export, close, properties. +use deckcraft_model::{Presentation, defaults}; use serde_json::{Value, json}; -use slidecraft_model::{Presentation, defaults}; use super::*; use crate::{DocState, EngineError, Result, Session}; @@ -11,11 +11,11 @@ pub fn specs() -> Vec { cmd!(noundo "file.new", "New Presentation", ["File"], Some("Cmd+N"), "{theme?: name, size?: [w,h] pt, blank?: bool}", always, new), cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"), "{path}", always, open), cmd!(noundo "file.openBytes", "Open Data", [], None, "{name, data: base64}", always, open_bytes), - cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?, format?: slidecraft|pptx}", has_doc, save), + cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?, format?: deckcraft|pptx}", has_doc, save), cmd!(noundo "file.saveAs", "Save As…", ["File"], Some("Cmd+Shift+S"), "{path, format?}", has_doc, save_as), cmd!(noundo "file.saveTemplate", "Save as Template…", ["File"], None, "{path}", has_doc, save_as), - cmd!(query "file.saveBytes", "Save to Data", [], None, "{format?: slidecraft|pptx} → {data: base64}", has_doc, save_bytes_cmd), - cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format: png|jpeg|pptx|slidecraft|outline|pdf, slide?: index, all?: bool, scale?: px per pt; pdf: layout?: slides|notes|handouts, perPage?: 1|2|3|4|6|9, dpi?, slides?: [index], includeHidden?, textLayer?, frame?}", has_doc, export), + cmd!(query "file.saveBytes", "Save to Data", [], None, "{format?: deckcraft|pptx} → {data: base64}", has_doc, save_bytes_cmd), + cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format: png|jpeg|pptx|deckcraft|outline|pdf, slide?: index, all?: bool, scale?: px per pt; pdf: layout?: slides|notes|handouts, perPage?: 1|2|3|4|6|9, dpi?, slides?: [index], includeHidden?, textLayer?, frame?}", has_doc, export), cmd!(query "file.render", "Render Slide", [], None, "{slide?: index, scale?, edit?: bool} → {png: base64, width, height}", has_doc, render), cmd!(noundo "file.close", "Close", ["File"], Some("Cmd+W"), "{}", has_doc, close), cmd!( @@ -36,8 +36,8 @@ pub fn specs() -> Vec { ] } -fn theme_named(name: &str) -> Option { - slidecraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name)) +fn theme_named(name: &str) -> Option { + deckcraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name)) } fn new(s: &mut Session, p: &Value) -> Result { @@ -45,7 +45,7 @@ fn new(s: &mut Session, p: &Value) -> Result { let size = p .get("size") .and_then(Value::as_array) - .and_then(|a| Some(slidecraft_geom::Size::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))) + .and_then(|a| Some(deckcraft_geom::Size::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))) .filter(|s| s.width >= 1.0 && s.height >= 1.0 && s.width < 5000.0 && s.height < 5000.0) .unwrap_or(defaults::WIDE); let doc = defaults::blank_presentation(size, theme, !bool_or(p, "blank", false)); @@ -56,25 +56,25 @@ fn new(s: &mut Session, p: &Value) -> Result { /// Recognise and read presentation bytes (native, PPTX). pub fn open_presentation(name: &str, bytes: &[u8]) -> Result { - if slidecraft_format::sniff(bytes) { - return slidecraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string())); + if deckcraft_format::sniff(bytes) { + return deckcraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string())); } - if slidecraft_pptx::sniff(bytes) { - let mut p = slidecraft_pptx::import(bytes).map_err(|e| EngineError::Other(e.to_string()))?; - slidecraft_format::repair(&mut p); + if deckcraft_pptx::sniff(bytes) { + let mut p = deckcraft_pptx::import(bytes).map_err(|e| EngineError::Other(e.to_string()))?; + deckcraft_format::repair(&mut p); return Ok(p); } let lower = name.to_ascii_lowercase(); if lower.ends_with(".txt") || lower.ends_with(".md") { let text = String::from_utf8_lossy(bytes); let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false); - slidecraft_format::outline_to_slides(&mut p, &text); + deckcraft_format::outline_to_slides(&mut p, &text); if p.slides.is_empty() { p = defaults::new_presentation(None); } return Ok(p); } - Err(EngineError::Other(format!("{name}: not a presentation SlideCraft can read"))) + Err(EngineError::Other(format!("{name}: not a presentation DeckCraft can read"))) } pub fn add_opened(s: &mut Session, name: &str, path: Option, p: Presentation) -> usize { @@ -87,7 +87,7 @@ fn open(s: &mut Session, p: &Value) -> Result { let path = str_param(p, "path").ok_or_else(|| bad("file.open", "missing `path`"))?; let bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?; let doc = open_presentation(path, &bytes)?; - let native = slidecraft_format::sniff(&bytes) || slidecraft_pptx::sniff(&bytes); + let native = deckcraft_format::sniff(&bytes) || deckcraft_pptx::sniff(&bytes); let i = add_opened(s, path, native.then(|| path.to_string()), doc); Ok(json!({"document": i, "slides": s.doc()?.doc.slides.len()})) } @@ -104,24 +104,24 @@ fn open_bytes(s: &mut Session, p: &Value) -> Result { Ok(json!({"document": i})) } -/// Encode the presentation in `format` (`slidecraft`, `pptx`, `outline`). +/// Encode the presentation in `format` (`deckcraft`, `pptx`, `outline`). pub fn save_bytes(doc: &Presentation, format: &str) -> Result> { match format { - "pptx" | "potx" | "ppsx" => slidecraft_pptx::export(doc).map_err(|e| EngineError::Other(e.to_string())), - "outline" | "txt" => Ok(slidecraft_format::slides_to_outline(doc).into_bytes()), - "pdf" => slidecraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())), - _ => slidecraft_format::save(doc).map_err(|e| EngineError::Other(e.to_string())), + "pptx" | "potx" | "ppsx" => deckcraft_pptx::export(doc).map_err(|e| EngineError::Other(e.to_string())), + "outline" | "txt" => Ok(deckcraft_format::slides_to_outline(doc).into_bytes()), + "pdf" => deckcraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())), + _ => deckcraft_format::save(doc).map_err(|e| EngineError::Other(e.to_string())), } } /// PDF options from command params: `{layout: slides|notes|handouts, perPage, dpi, slides: [i], /// includeHidden, textLayer, frame}`. -pub fn pdf_options(p: &Value) -> slidecraft_pdf::PdfOptions { - let mut o = slidecraft_pdf::PdfOptions { layout: slidecraft_pdf::PageLayout::Slides, ..Default::default() }; +pub fn pdf_options(p: &Value) -> deckcraft_pdf::PdfOptions { + let mut o = deckcraft_pdf::PdfOptions { layout: deckcraft_pdf::PageLayout::Slides, ..Default::default() }; o.layout = match str_param(p, "layout").unwrap_or("slides") { - "notes" => slidecraft_pdf::PageLayout::Notes, - "handouts" => slidecraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).min(9) as u8 }, - _ => slidecraft_pdf::PageLayout::Slides, + "notes" => deckcraft_pdf::PageLayout::Notes, + "handouts" => deckcraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).min(9) as u8 }, + _ => deckcraft_pdf::PageLayout::Slides, }; o.dpi = f64_or(p, "dpi", o.dpi).clamp(36.0, 600.0); o.slides = p.get("slides").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_u64().map(|x| x as usize)).collect()); @@ -133,7 +133,7 @@ pub fn pdf_options(p: &Value) -> slidecraft_pdf::PdfOptions { /// PDF bytes for `doc` with [`pdf_options`] from `p`. pub fn pdf_bytes(doc: &Presentation, p: &Value) -> Result> { - slidecraft_pdf::export(doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string())) + deckcraft_pdf::export(doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string())) } pub fn format_for_path(path: &str) -> &'static str { @@ -149,7 +149,7 @@ pub fn format_for_path(path: &str) -> &'static str { } else if l.ends_with(".txt") || l.ends_with(".md") { "outline" } else { - "slidecraft" + "deckcraft" } } @@ -201,7 +201,7 @@ fn save_as(s: &mut Session, p: &Value) -> Result { } fn save_bytes_cmd(s: &mut Session, p: &Value) -> Result { - let format = str_param(p, "format").unwrap_or("slidecraft"); + let format = str_param(p, "format").unwrap_or("deckcraft"); let bytes = save_bytes(&s.doc()?.doc, format)?; Ok(json!({"data": base64_encode(&bytes), "bytes": bytes.len()})) } @@ -209,7 +209,7 @@ fn save_bytes_cmd(s: &mut Session, p: &Value) -> Result { /// Render slide `index` (current when absent) to PNG bytes. pub fn render_png(doc: &Presentation, index: usize, scale: f64, edit: bool) -> (Vec, u32, u32) { let img = - slidecraft_render::render_slide(doc, index, &slidecraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), edit, ..Default::default() }); + deckcraft_render::render_slide(doc, index, &deckcraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), edit, ..Default::default() }); (img.to_png(), img.width, img.height) } @@ -240,10 +240,10 @@ fn export(s: &mut Session, p: &Value) -> Result { if *i >= st.doc.slides.len() { return Err(bad("file.export", format!("no slide {i}"))); } - let img = slidecraft_render::render_slide( + let img = deckcraft_render::render_slide( &st.doc, *i, - &slidecraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), ..Default::default() }, + &deckcraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), ..Default::default() }, ); let bytes = if format == "png" { img.to_png() } else { img.to_jpeg(92) }; let out = if slides.len() == 1 { @@ -260,7 +260,7 @@ fn export(s: &mut Session, p: &Value) -> Result { Ok(json!({"files": written})) } "pdf" => { - let bytes = slidecraft_pdf::export(&st.doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string()))?; + let bytes = deckcraft_pdf::export(&st.doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string()))?; write_file(&path, &bytes)?; Ok(json!({"path": path, "bytes": bytes.len()})) } diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 9841dc5..fc97100 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -1,11 +1,11 @@ //! Home ▸ Font and Paragraph, Format menu: character, paragraph and text box formatting, and //! the Format Painter. +use deckcraft_model::edit as ed; +use deckcraft_model::resolve; +use deckcraft_model::text::{Align, Anchor, AutoFit, Bullet, Caps, ParaProps, RunProps, Spacing, Strike, TextDir}; +use deckcraft_model::{Effects, Fill, Line, Shape}; use serde_json::{Value, json}; -use slidecraft_model::edit as ed; -use slidecraft_model::resolve; -use slidecraft_model::text::{Align, Anchor, AutoFit, Bullet, Caps, ParaProps, RunProps, Spacing, Strike, TextDir}; -use slidecraft_model::{Effects, Fill, Line, Shape}; use super::text::{format_body, format_para, format_run}; use super::*; @@ -289,8 +289,8 @@ fn clear(s: &mut Session, _p: &Value) -> Result { } fn text_shadow(s: &mut Session, p: &Value) -> Result { let v = on_param(p, Some(current_run(s).shadow.is_some())); - let sh = slidecraft_model::style::Shadow { - color: slidecraft_model::ColorRef::rgb(slidecraft_color::Rgba::BLACK).with(slidecraft_color::ColorTransform::Alpha(43000)), + let sh = deckcraft_model::style::Shadow { + color: deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::BLACK).with(deckcraft_color::ColorTransform::Alpha(43000)), blur: 3.0, dist: 2.0, dir: 45.0, @@ -328,7 +328,7 @@ fn case(s: &mut Session, p: &Value) -> Result { let mut pos = 0; for r in &mut para.runs { let len = r.char_len(); - if r.kind == slidecraft_model::text::RunKind::Text { + if r.kind == deckcraft_model::text::RunKind::Text { let rs = from.max(pos).saturating_sub(pos); let re = to.min(pos + len).saturating_sub(pos); if re > rs { @@ -349,7 +349,7 @@ fn case(s: &mut Session, p: &Value) -> Result { if let Some(t) = sh.text.as_mut() { for para in &mut t.paragraphs { for r in &mut para.runs { - if r.kind == slidecraft_model::text::RunKind::Text { + if r.kind == deckcraft_model::text::RunKind::Text { r.text = ed::change_case(&r.text, &mode); } } @@ -541,7 +541,7 @@ pub struct Painted { pub fill: Option, pub line: Option, pub effects: Option, - pub style: Option, + pub style: Option, } fn painter_pick(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index cd0fed7..f2be6b8 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -1,12 +1,12 @@ //! Insert tab: shapes, text boxes, pictures, tables, charts, audio, video, WordArt, fields. +use deckcraft_color::SchemeSlot; +use deckcraft_geom::Xfrm; +use deckcraft_model::chart::ChartType; +use deckcraft_model::style::PictureFill; +use deckcraft_model::text::{AutoFit, RunKind, TextBody}; +use deckcraft_model::{Chart, ColorRef, Fill, Geom, MediaClip, Shape, ShapeId, ShapeKind, ShapeStyle, Table}; use serde_json::{Value, json}; -use slidecraft_color::SchemeSlot; -use slidecraft_geom::Xfrm; -use slidecraft_model::chart::ChartType; -use slidecraft_model::style::PictureFill; -use slidecraft_model::text::{AutoFit, RunKind, TextBody}; -use slidecraft_model::{Chart, ColorRef, Fill, Geom, MediaClip, Shape, ShapeId, ShapeKind, ShapeStyle, Table}; use super::*; use crate::{EngineError, Result, Session}; @@ -92,7 +92,7 @@ pub fn specs() -> Vec { /// Default size for a click-inserted shape (1 inch square, PowerPoint style). fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm { - let size = s.active().map(|d| d.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE); + let size = s.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE); Xfrm::new((size.width - w) / 2.0, (size.height - h) / 2.0, w, h) } @@ -110,7 +110,7 @@ pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Resu _ => shape .geom .preset_name() - .and_then(slidecraft_geom::preset::info) + .and_then(deckcraft_geom::preset::info) .map(|i| i.label.split(':').next().unwrap_or(i.label).trim().to_string()) .unwrap_or_else(|| "Shape".into()), }; @@ -130,8 +130,8 @@ pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Resu fn insert_shape(s: &mut Session, p: &Value) -> Result { let preset = str_param(p, "preset").unwrap_or("rect"); - let info = slidecraft_geom::preset::info(preset).ok_or_else(|| bad("shape.insert", format!("unknown preset `{preset}`")))?; - let line_like = slidecraft_geom::preset::is_line_like(preset); + let info = deckcraft_geom::preset::info(preset).ok_or_else(|| bad("shape.insert", format!("unknown preset `{preset}`")))?; + let line_like = deckcraft_geom::preset::is_line_like(preset); let rect = rect_param(p, "rect").unwrap_or_else(|| default_rect(s, 72.0, if line_like { 0.0 } else { 72.0 })); let adj: Vec = p.get("adj").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default(); let mut shape = Shape { @@ -153,14 +153,14 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result { } if let Some(tb) = shape.text.as_mut() { for para in &mut tb.paragraphs { - para.props.align = Some(slidecraft_model::text::Align::Center); + para.props.align = Some(deckcraft_model::text::Align::Center); } } } else { shape.kind = if preset.contains("Connector") { ShapeKind::Connector { start: None, end: None } } else { ShapeKind::Shape }; if preset == "straightConnector1" { - shape.line = Some(slidecraft_model::Line { - tail: Some(slidecraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() }), + shape.line = Some(deckcraft_model::Line { + tail: Some(deckcraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() }), ..Default::default() }); } @@ -174,7 +174,7 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result { fn presets(_s: &mut Session, _p: &Value) -> Result { Ok(Value::Array( - slidecraft_geom::preset::CATALOG.iter().map(|i| json!({"name": i.name, "label": i.label, "category": i.category.label()})).collect(), + deckcraft_geom::preset::CATALOG.iter().map(|i| json!({"name": i.name, "label": i.label, "category": i.category.label()})).collect(), )) } @@ -287,7 +287,7 @@ fn fitted(s: &Session, bytes: &[u8]) -> (f64, f64) { .and_then(|r| r.into_dimensions().ok()) .map(|(w, h)| (w as f64 * 0.75, h as f64 * 0.75)) .unwrap_or((288.0, 216.0)); - let size = s.active().map(|d| d.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE); + let size = s.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE); let k = (size.width * 0.9 / w.max(1.0)).min(size.height * 0.9 / h.max(1.0)).min(1.0); (w * k, h * k) } @@ -298,7 +298,7 @@ fn picture(s: &mut Session, p: &Value) -> Result { if !ct.starts_with("image/") { return Err(bad("insert.picture", format!("{name} is not a picture"))); } - if ct != "image/svg+xml" && slidecraft_render::decode_image(&bytes).is_none() { + if ct != "image/svg+xml" && deckcraft_render::decode_image(&bytes).is_none() { return Err(EngineError::Other(format!("{name}: the picture can't be read"))); } let (w, h) = fitted(s, &bytes); @@ -308,7 +308,7 @@ fn picture(s: &mut Session, p: &Value) -> Result { .into_iter() .find(|x| { x.ph_type() - .is_some_and(|k| matches!(k, slidecraft_model::PhType::Picture | slidecraft_model::PhType::Obj | slidecraft_model::PhType::Body)) + .is_some_and(|k| matches!(k, deckcraft_model::PhType::Picture | deckcraft_model::PhType::Obj | deckcraft_model::PhType::Body)) && x.text.as_ref().is_none_or(|t| t.is_empty()) }) .map(|x| (x.id, xfrm_of(&d.doc, &d.selection, x))) @@ -349,7 +349,7 @@ fn picture(s: &mut Session, p: &Value) -> Result { sh.kind = shape.kind.clone(); sh.xfrm = Some(rect); sh.ph = phv.map(|mut p| { - p.kind = slidecraft_model::PhType::Picture; + p.kind = deckcraft_model::PhType::Picture; p }); sh.text = None; @@ -426,7 +426,7 @@ fn chart(s: &mut Session, p: &Value) -> Result { if let Some(series) = p.get("series").and_then(Value::as_array) { c.series = series .iter() - .map(|sv| slidecraft_model::chart::Series { + .map(|sv| deckcraft_model::chart::Series { name: sv.get("name").and_then(Value::as_str).unwrap_or("Series").to_string(), values: sv.get("values").and_then(Value::as_array).map(|a| a.iter().map(Value::as_f64).collect()).unwrap_or_default(), ..Default::default() @@ -447,7 +447,7 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result { let (name, bytes) = media_bytes(p, cmd)?; let ct = content_type(&name, &bytes); let bytes = std::sync::Arc::new(bytes); - let info = slidecraft_media::probe(&bytes); + let info = deckcraft_media::probe(&bytes); let has_video = info.as_ref().is_ok_and(|i| i.video.is_some()); let has_audio = info.as_ref().is_ok_and(|i| i.audio.is_some()); match &info { @@ -521,7 +521,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result { let mut shape = new_text_box(Xfrm::new(size.width * 0.2, size.height * 0.4, size.width * 0.6, 80.0), text); if let Some(t) = shape.text.as_mut() { for para in &mut t.paragraphs { - para.props.align = Some(slidecraft_model::text::Align::Center); + para.props.align = Some(deckcraft_model::text::Align::Center); for r in &mut para.runs { r.props.size = Some(54.0); r.props.bold = Some(style % 2 == 1); @@ -532,7 +532,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result { _ => SchemeSlot::Accent4, }))); if style % 3 == 2 { - r.props.outline = Some(slidecraft_model::Line::solid(ColorRef::scheme(SchemeSlot::Bg1), 1.0)); + r.props.outline = Some(deckcraft_model::Line::solid(ColorRef::scheme(SchemeSlot::Bg1), 1.0)); } } } @@ -568,7 +568,7 @@ fn symbol(s: &mut Session, p: &Value) -> Result { } fn hyperlink(s: &mut Session, p: &Value) -> Result { - use slidecraft_model::text::{Action, Hyperlink}; + use deckcraft_model::text::{Action, Hyperlink}; let action = if let Some(u) = str_param(p, "url") { Action::Url { url: u.to_string() } } else if let Some(i) = usize_param(p, "slide") { @@ -589,7 +589,7 @@ fn hyperlink(s: &mut Session, p: &Value) -> Result { } fn action_button(s: &mut Session, p: &Value) -> Result { - use slidecraft_model::text::{Action, Hyperlink}; + use deckcraft_model::text::{Action, Hyperlink}; let kind = str_param(p, "kind").unwrap_or("custom"); let (preset, action) = match kind { "back" | "previous" => ("actionButtonBackPrevious", Some(Action::PreviousSlide)), @@ -644,9 +644,9 @@ fn chart_data(s: &mut Session, p: &Value) -> Result { .get("categories") .and_then(Value::as_array) .map(|a| a.iter().map(|v| v.as_str().map(String::from).unwrap_or_else(|| v.to_string())).collect()); - let series: Option> = p.get("series").and_then(Value::as_array).map(|a| { + let series: Option> = p.get("series").and_then(Value::as_array).map(|a| { a.iter() - .map(|sv| slidecraft_model::chart::Series { + .map(|sv| deckcraft_model::chart::Series { name: sv.get("name").and_then(Value::as_str).unwrap_or("Series").to_string(), values: sv.get("values").and_then(Value::as_array).map(|v| v.iter().map(Value::as_f64).collect()).unwrap_or_default(), ..Default::default() @@ -773,7 +773,7 @@ fn smart_art(s: &mut Session, p: &Value) -> Result { let mut body = TextBody::from_text(items.get(i).map(String::as_str).unwrap_or("")); body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }); for para in &mut body.paragraphs { - para.props.align = Some(slidecraft_model::text::Align::Center); + para.props.align = Some(deckcraft_model::text::Align::Center); for r in &mut para.runs { r.props.size = Some(20.0); } diff --git a/crates/engine/src/cmd/inspect.rs b/crates/engine/src/cmd/inspect.rs index 6b9bd18..db162a3 100644 --- a/crates/engine/src/cmd/inspect.rs +++ b/crates/engine/src/cmd/inspect.rs @@ -1,8 +1,8 @@ //! Inspection for agents: the deck, slides and shapes as JSON (verify work without screenshots). +use deckcraft_model::resolve::{self, Ctx}; +use deckcraft_model::{Shape, ShapeKind}; use serde_json::{Value, json}; -use slidecraft_model::resolve::{self, Ctx}; -use slidecraft_model::{Shape, ShapeKind}; use super::*; use crate::{Result, Session}; @@ -51,8 +51,8 @@ pub fn shape_summary(ctx: Option<&Ctx>, sh: &Shape, depth: usize) -> Value { let fill = ctx.map(|c| { let (f, ph) = resolve::fill(c, sh); match f { - Some(slidecraft_model::Fill::Solid { color }) => json!(format!("#{}", c.color(&color, ph).hex())), - Some(slidecraft_model::Fill::None) | None => json!("none"), + Some(deckcraft_model::Fill::Solid { color }) => json!(format!("#{}", c.color(&color, ph).hex())), + Some(deckcraft_model::Fill::None) | None => json!("none"), Some(other) => json!(serde_json::to_value(&other).ok().and_then(|v| v.get("kind").cloned()).unwrap_or_default()), } }); @@ -116,7 +116,7 @@ fn slide(s: &mut Session, p: &Value) -> Result { let ctx = Ctx::for_slide(&st.doc, sl); let shapes: Vec = sl.shapes.iter().map(|sh| shape_summary(ctx.as_ref(), sh, 0)).collect(); let bg = ctx.as_ref().map(|c| match resolve::background(c, Some(sl)) { - (slidecraft_model::Fill::Solid { color }, ph) => json!(format!("#{}", c.color(&color, ph).hex())), + (deckcraft_model::Fill::Solid { color }, ph) => json!(format!("#{}", c.color(&color, ph).hex())), (f, _) => serde_json::to_value(&f).ok().and_then(|v| v.get("kind").cloned()).unwrap_or_default(), }); Ok(json!({ diff --git a/crates/engine/src/cmd/media.rs b/crates/engine/src/cmd/media.rs index 1e51fe8..5402ad8 100644 --- a/crates/engine/src/cmd/media.rs +++ b/crates/engine/src/cmd/media.rs @@ -2,8 +2,8 @@ //! requests the UI host carries out with its player ([`crate::UiRequest::Media`]); the host reports //! back through [`Session::media_status`], which `media.info` returns with the probe. +use deckcraft_model::{MediaClip, ShapeId, ShapeKind}; use serde_json::{Value, json}; -use slidecraft_model::{MediaClip, ShapeId, ShapeKind}; use super::*; use crate::{EngineError, MediaStatus, Result, Session, UiRequest}; @@ -83,7 +83,7 @@ fn seek(s: &mut Session, p: &Value) -> Result { request(s, p, "media.seek", "seek", Some(ms)) } -pub(crate) fn probe_json(info: &slidecraft_media::MediaInfo) -> Value { +pub(crate) fn probe_json(info: &deckcraft_media::MediaInfo) -> Value { json!({ "container": info.container, "durationMs": info.duration_ms, @@ -97,7 +97,7 @@ fn info(s: &mut Session, p: &Value) -> Result { let (id, m) = target(s, p, "media.info")?; let st = s.doc()?; let item = st.doc.media(m.media).ok_or_else(|| EngineError::Other("the media data is missing".into()))?; - let probe = slidecraft_media::probe(&item.data).map(|i| probe_json(&i)).unwrap_or_else(|e| json!({"error": e.to_string()})); + let probe = deckcraft_media::probe(&item.data).map(|i| probe_json(&i)).unwrap_or_else(|e| json!({"error": e.to_string()})); Ok(json!({ "id": id, "name": item.name, @@ -119,8 +119,8 @@ fn info(s: &mut Session, p: &Value) -> Result { } /// A poster frame PNG for video bytes at `ms`. -pub(crate) fn poster_png(bytes: &slidecraft_media::Bytes, ms: u64) -> Option> { - match slidecraft_media::poster_png(bytes, ms as f64 / 1000.0) { +pub(crate) fn poster_png(bytes: &deckcraft_media::Bytes, ms: u64) -> Option> { + match deckcraft_media::poster_png(bytes, ms as f64 / 1000.0) { Ok(png) => Some(png), Err(e) => { log::info!("no poster frame: {e}"); diff --git a/crates/engine/src/cmd/merge.rs b/crates/engine/src/cmd/merge.rs index 207e3fe..4c9a6d2 100644 --- a/crates/engine/src/cmd/merge.rs +++ b/crates/engine/src/cmd/merge.rs @@ -2,12 +2,12 @@ //! the outlines of the selected shapes. The result takes the formatting of the first selected //! shape, like PowerPoint. +use deckcraft_geom::{PathEl, Rect, Xfrm}; +use deckcraft_model::{CustomPath, Geom, Shape, ShapeId, ShapeKind}; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay_rule::OverlayRule; use i_overlay::float::single::SingleFloatOverlay; use serde_json::{Value, json}; -use slidecraft_geom::{PathEl, Rect, Xfrm}; -use slidecraft_model::{CustomPath, Geom, Shape, ShapeId, ShapeKind}; use super::*; use crate::{Result, Session}; @@ -31,12 +31,12 @@ pub fn specs() -> Vec { } /// The filled outline of `s` in slide points (curves flattened). -fn area_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, s: &Shape) -> Area { +fn area_of(doc: &deckcraft_model::Presentation, sel: &crate::Selection, s: &Shape) -> Area { let x = xfrm_of(doc, sel, s); - let geo = slidecraft_render::shape_geometry(s, x.w, x.h); + let geo = deckcraft_render::shape_geometry(s, x.w, x.h); let a = x.affine(); let mut out: Area = vec![]; - for sp in geo.paths.iter().filter(|p| p.fill != slidecraft_geom::preset::FillMode::None) { + for sp in geo.paths.iter().filter(|p| p.fill != deckcraft_geom::preset::FillMode::None) { let mut cur: Vec<[f64; 2]> = vec![]; let flush = |cur: &mut Vec<[f64; 2]>, out: &mut Area| { if cur.len() > 2 { @@ -45,7 +45,7 @@ fn area_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, s: &Sha cur.clear(); } }; - slidecraft_geom::preset_flatten(&sp.path, &mut |el| match el { + deckcraft_geom::preset_flatten(&sp.path, &mut |el| match el { PathEl::MoveTo(p) => { flush(&mut cur, &mut out); let q = a * p; diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index e42ae21..a337107 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -17,10 +17,10 @@ pub mod table; pub mod text; pub mod transition; +use deckcraft_geom::Xfrm; +use deckcraft_model::ShapeId; use serde::Serialize; use serde_json::Value; -use slidecraft_geom::Xfrm; -use slidecraft_model::ShapeId; use crate::{EngineError, Result, Session}; @@ -207,31 +207,31 @@ pub(crate) fn ok() -> Result { } /// Parse a colour param: `"#RRGGBB"`, `"accent1"`…, or `{"scheme":"accent2","lumMod":75000}`. -pub(crate) fn color_param(p: &Value, key: &str) -> Option { +pub(crate) fn color_param(p: &Value, key: &str) -> Option { let v = p.get(key)?; color_value(v) } -pub(crate) fn color_value(v: &Value) -> Option { - use slidecraft_model::ColorRef; +pub(crate) fn color_value(v: &Value) -> Option { + use deckcraft_model::ColorRef; match v { Value::String(s) => { - if let Some(slot) = slidecraft_color::SchemeSlot::from_xml(s) { + if let Some(slot) = deckcraft_color::SchemeSlot::from_xml(s) { return Some(ColorRef::scheme(slot)); } - if let Some(c) = slidecraft_color::Rgba::from_hex(s) { + if let Some(c) = deckcraft_color::Rgba::from_hex(s) { return Some(ColorRef::rgb(c)); } - slidecraft_color::preset(s).map(ColorRef::rgb) + deckcraft_color::preset(s).map(ColorRef::rgb) } Value::Object(o) => { let mut c = if let Some(s) = o.get("scheme").and_then(Value::as_str) { - ColorRef::scheme(slidecraft_color::SchemeSlot::from_xml(s)?) + ColorRef::scheme(deckcraft_color::SchemeSlot::from_xml(s)?) } else { - ColorRef::rgb(slidecraft_color::Rgba::from_hex(o.get("rgb").and_then(Value::as_str)?)?) + ColorRef::rgb(deckcraft_color::Rgba::from_hex(o.get("rgb").and_then(Value::as_str)?)?) }; for (k, val) in o { - if let Some(t) = slidecraft_color::ColorTransform::from_xml(k, val.as_i64().and_then(|x| i32::try_from(x).ok())) { + if let Some(t) = deckcraft_color::ColorTransform::from_xml(k, val.as_i64().and_then(|x| i32::try_from(x).ok())) { c.mods.push(t); } } @@ -242,20 +242,20 @@ pub(crate) fn color_value(v: &Value) -> Option { } /// Look a shape up by id in the edited tree. -pub(crate) fn shape_of(s: &Session, id: ShapeId) -> Result { +pub(crate) fn shape_of(s: &Session, id: ShapeId) -> Result { s.doc()?.shape(id).cloned().ok_or_else(|| EngineError::Other(format!("no shape {id}"))) } /// Effective box of a shape (placeholders inherit theirs). -pub fn xfrm_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape: &slidecraft_model::Shape) -> Xfrm { +pub fn xfrm_of(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm { if let Some(x) = shape.xfrm { return x; } ctx_xfrm(doc, sel, shape) } -fn ctx_xfrm(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape: &slidecraft_model::Shape) -> Xfrm { - use slidecraft_model::resolve::{self, Ctx}; +fn ctx_xfrm(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm { + use deckcraft_model::resolve::{self, Ctx}; match sel.target { crate::Target::Slides => doc.slides.get(sel.slide).and_then(|s| Ctx::for_slide(doc, s)).map(|c| resolve::xfrm(&c, shape)), crate::Target::Master { master } => doc.masters.get(master).map(|m| resolve::xfrm(&Ctx::for_master(doc, m), shape)), @@ -267,21 +267,21 @@ fn ctx_xfrm(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape: } /// Run `f` on each target shape (copy-on-write), materialising inherited boxes first. -pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut slidecraft_model::Shape) -> Result<()>) -> Result { +pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut deckcraft_model::Shape) -> Result<()>) -> Result { let ids = targets(s, p)?; if ids.is_empty() { return Err(bad(cmd, "no shapes selected")); } s.edit(|doc, sel| { let boxes: Vec<(ShapeId, Xfrm)> = { - let st_shapes: Vec = - ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| slidecraft_model::find_shape(v, *id)).cloned()).collect(); + let st_shapes: Vec = + ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)).cloned()).collect(); st_shapes.iter().map(|sh| (sh.id, xfrm_of(doc, sel, sh))).collect() }; let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?; let mut n = 0; for id in &ids { - if let Some(sh) = slidecraft_model::find_shape_mut(shapes, *id) { + if let Some(sh) = deckcraft_model::find_shape_mut(shapes, *id) { if sh.xfrm.is_none() { sh.xfrm = boxes.iter().find(|(i, _)| i == id).map(|(_, x)| *x); } @@ -296,7 +296,7 @@ pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut }) } -pub(crate) fn current_shapes<'a>(doc: &'a slidecraft_model::Presentation, sel: &crate::Selection) -> Option<&'a Vec> { +pub(crate) fn current_shapes<'a>(doc: &'a deckcraft_model::Presentation, sel: &crate::Selection) -> Option<&'a Vec> { match sel.target { crate::Target::Slides => doc.slides.get(sel.slide).map(|s| &s.shapes), crate::Target::Master { master } => doc.masters.get(master).map(|m| &m.shapes), diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 20356a9..8e3a532 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -2,8 +2,8 @@ use std::sync::Arc; +use deckcraft_model::Comment; use serde_json::{Value, json}; -use slidecraft_model::Comment; use super::*; use crate::{Result, Session, UiRequest}; @@ -130,34 +130,34 @@ fn accessibility(s: &mut Session, _p: &Value) -> Result { if sl.title().trim().is_empty() { out.push(json!({"slide": i, "issue": "Missing slide title"})); } - slidecraft_model::walk(&sl.shapes, &mut |sh, _| { + deckcraft_model::walk(&sl.shapes, &mut |sh, _| { let needs_alt = matches!( sh.kind, - slidecraft_model::ShapeKind::Picture { .. } - | slidecraft_model::ShapeKind::Chart(_) - | slidecraft_model::ShapeKind::Media(_) - | slidecraft_model::ShapeKind::Group { .. } + deckcraft_model::ShapeKind::Picture { .. } + | deckcraft_model::ShapeKind::Chart(_) + | deckcraft_model::ShapeKind::Media(_) + | deckcraft_model::ShapeKind::Group { .. } ); if needs_alt && sh.descr.trim().is_empty() && !sh.decorative { out.push(json!({"slide": i, "shape": sh.id, "name": sh.name, "issue": "Missing alternative text"})); } }); // Low-contrast text against the slide background. - if let Some(ctx) = slidecraft_model::resolve::Ctx::for_slide(&st.doc, sl) { - let bg = match slidecraft_model::resolve::background(&ctx, Some(sl)) { - (slidecraft_model::Fill::Solid { color }, ph) => Some(ctx.color(&color, ph)), + if let Some(ctx) = deckcraft_model::resolve::Ctx::for_slide(&st.doc, sl) { + let bg = match deckcraft_model::resolve::background(&ctx, Some(sl)) { + (deckcraft_model::Fill::Solid { color }, ph) => Some(ctx.color(&color, ph)), _ => None, }; if let Some(bg) = bg { for sh in &sl.shapes { let Some(t) = &sh.text else { continue }; let Some(para) = t.paragraphs.iter().find(|p| !p.is_empty()) else { continue }; - let rp = slidecraft_model::resolve::run(&ctx, sh, para, ¶.runs.first().map(|r| r.props.clone()).unwrap_or_default()); - let (fill, _) = slidecraft_model::resolve::fill(&ctx, sh); + let rp = deckcraft_model::resolve::run(&ctx, sh, para, ¶.runs.first().map(|r| r.props.clone()).unwrap_or_default()); + let (fill, _) = deckcraft_model::resolve::fill(&ctx, sh); if fill.as_ref().is_some_and(|f| !f.is_none()) { continue; } - let fg = slidecraft_model::resolve::text_color(&ctx, &rp); + let fg = deckcraft_model::resolve::text_color(&ctx, &rp); if fg.contrast(bg) < 3.0 { out.push(json!({"slide": i, "shape": sh.id, "name": sh.name, "issue": format!("Hard-to-read text contrast ({:.1}:1)", fg.contrast(bg))})); } @@ -201,7 +201,7 @@ fn spelling(s: &mut Session, _p: &Value) -> Result { let st = s.doc()?; let mut out = vec![]; for (i, sl) in st.doc.slides.iter().enumerate() { - slidecraft_model::walk(&sl.shapes, &mut |sh, _| { + deckcraft_model::walk(&sl.shapes, &mut |sh, _| { if let Some(t) = &sh.text { for w in t.text().split(|c: char| !c.is_alphanumeric() && c != '\'') { if COMMON.contains(&w.to_lowercase().as_str()) { @@ -258,7 +258,7 @@ fn custom_show(s: &mut Session, p: &Value) -> Result { doc.custom_shows.retain(|c| c.name != name); if !delete { let slides = idx.iter().filter_map(|i| doc.slides.get(*i).map(|x| x.id)).collect(); - doc.custom_shows.push(slidecraft_model::CustomShow { name: name.clone(), slides }); + doc.custom_shows.push(deckcraft_model::CustomShow { name: name.clone(), slides }); } Ok(()) })?; diff --git a/crates/engine/src/cmd/shape.rs b/crates/engine/src/cmd/shape.rs index 10ac46f..7e12f60 100644 --- a/crates/engine/src/cmd/shape.rs +++ b/crates/engine/src/cmd/shape.rs @@ -1,10 +1,10 @@ //! Shape Format: position and size, rotation, flips, geometry, fill, outline, effects, styles, //! names and alt text. +use deckcraft_color::{ColorTransform, Rgba, SchemeSlot}; +use deckcraft_model::style::{Dash, Glow, Gradient, GradientShape, GradientStop, LineEnd, PatternFill, PictureFill, Reflection, Shadow}; +use deckcraft_model::{ColorRef, Effects, Fill, Geom, Line, ShapeStyle}; use serde_json::{Value, json}; -use slidecraft_color::{ColorTransform, Rgba, SchemeSlot}; -use slidecraft_model::style::{Dash, Glow, Gradient, GradientShape, GradientStop, LineEnd, PatternFill, PictureFill, Reflection, Shadow}; -use slidecraft_model::{ColorRef, Effects, Fill, Geom, Line, ShapeStyle}; use super::*; use crate::{Result, Session}; @@ -195,7 +195,7 @@ fn adjust(s: &mut Session, p: &Value) -> Result { let v = f64_param(p, "value").ok_or_else(|| bad("shape.adjust", "missing `value`"))?; edit_shapes(s, p, "shape.adjust", |sh| { if let Geom::Preset { name, adj } = &mut sh.geom { - let defaults = slidecraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]); + let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]); while adj.len() <= i { let k = adj.len(); adj.push(defaults.get(k).copied().unwrap_or(0.0)); @@ -210,7 +210,7 @@ fn adjust(s: &mut Session, p: &Value) -> Result { fn change(s: &mut Session, p: &Value) -> Result { let name = str_param(p, "preset").ok_or_else(|| bad("shape.change", "missing `preset`"))?; - let info = slidecraft_geom::preset::info(name).ok_or_else(|| bad("shape.change", format!("unknown preset `{name}`")))?; + let info = deckcraft_geom::preset::info(name).ok_or_else(|| bad("shape.change", format!("unknown preset `{name}`")))?; edit_shapes(s, p, "shape.change", |sh| { sh.geom = Geom::preset(info.name); Ok(()) @@ -287,7 +287,7 @@ fn fill(s: &mut Session, p: &Value) -> Result { } // Transparency on an inherited (style) fill needs the effective colour. let st = s.doc()?; - let ctx_fill: Vec<(slidecraft_model::ShapeId, Option)> = { + let ctx_fill: Vec<(deckcraft_model::ShapeId, Option)> = { let ctx = super::text::ctx_for(&st.doc, &st.selection); targets(s, p)? .into_iter() @@ -296,7 +296,7 @@ fn fill(s: &mut Session, p: &Value) -> Result { ( id, ctx.as_ref().and_then(|c| { - let (f, ph) = slidecraft_model::resolve::fill(c, sh); + let (f, ph) = deckcraft_model::resolve::fill(c, sh); f.map(|f| match f { Fill::Solid { color } => Fill::solid(ColorRef::rgb(c.color(&color, ph))), other => other, @@ -338,14 +338,14 @@ fn line(s: &mut Session, p: &Value) -> Result { let head = str_param(p, "head").map(line_end); let tail = str_param(p, "tail").map(line_end); let cap = str_param(p, "cap").map(|c| match c { - "round" => slidecraft_model::style::LineCap::Round, - "square" => slidecraft_model::style::LineCap::Square, - _ => slidecraft_model::style::LineCap::Flat, + "round" => deckcraft_model::style::LineCap::Round, + "square" => deckcraft_model::style::LineCap::Square, + _ => deckcraft_model::style::LineCap::Flat, }); let join = str_param(p, "join").map(|c| match c { - "bevel" => slidecraft_model::style::LineJoin::Bevel, - "miter" => slidecraft_model::style::LineJoin::Miter, - _ => slidecraft_model::style::LineJoin::Round, + "bevel" => deckcraft_model::style::LineJoin::Bevel, + "miter" => deckcraft_model::style::LineJoin::Miter, + _ => deckcraft_model::style::LineJoin::Round, }); edit_shapes(s, p, "shape.line", |sh| { if reset { @@ -536,7 +536,7 @@ fn quick_style(s: &mut Session, p: &Value) -> Result { sh.line.clone_from(&l); sh.effects = None; if let Some(t) = sh.text.as_mut() { - slidecraft_model::edit::format_all(t, &|r| r.fill = None); + deckcraft_model::edit::format_all(t, &|r| r.fill = None); } Ok(()) }) @@ -596,7 +596,7 @@ fn set_default(s: &mut Session, _p: &Value) -> Result { fn crop(s: &mut Session, p: &Value) -> Result { let c = [f64_param(p, "left"), f64_param(p, "top"), f64_param(p, "right"), f64_param(p, "bottom")]; edit_shapes(s, p, "picture.crop", |sh| { - if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind { + if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind { for (i, v) in c.iter().enumerate() { if let (Some(v), Some(slot)) = (v, fill.crop.get_mut(i)) { *slot = v.clamp(-10.0, 0.95); @@ -609,7 +609,7 @@ fn crop(s: &mut Session, p: &Value) -> Result { fn picture_adjust(s: &mut Session, p: &Value) -> Result { edit_shapes(s, p, "picture.adjust", |sh| { - if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind { + if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind { let a = &mut fill.adjust; if bool_or(p, "reset", false) { *a = Default::default(); @@ -636,7 +636,7 @@ fn picture_adjust(s: &mut Session, p: &Value) -> Result { fn picture_reset(s: &mut Session, p: &Value) -> Result { edit_shapes(s, p, "picture.reset", |sh| { - if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind { + if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind { fill.adjust = Default::default(); fill.crop = [0.0; 4]; fill.alpha = None; @@ -652,7 +652,7 @@ fn picture_change(s: &mut Session, p: &Value) -> Result { let ct = super::insert::content_type(&name, &bytes); let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?; edit_shapes(s, p, "picture.change", |sh| { - if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind { + if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind { fill.media = media; fill.crop = [0.0; 4]; } @@ -662,7 +662,7 @@ fn picture_change(s: &mut Session, p: &Value) -> Result { fn media_options(s: &mut Session, p: &Value) -> Result { edit_shapes(s, p, "media.options", |sh| { - if let slidecraft_model::ShapeKind::Media(m) = &mut sh.kind { + if let deckcraft_model::ShapeKind::Media(m) = &mut sh.kind { if let Some(v) = bool_param(p, "autoplay") { m.autoplay = v; } @@ -703,8 +703,8 @@ fn media_options(s: &mut Session, p: &Value) -> Result { Ok(json!({})) } -fn shape_id(p: &Value, key: &str) -> Option { - p.get(key).and_then(Value::as_u64).map(|v| slidecraft_model::ShapeId(v as u32)) +fn shape_id(p: &Value, key: &str) -> Option { + p.get(key).and_then(Value::as_u64).map(|v| deckcraft_model::ShapeId(v as u32)) } fn sites(s: &mut Session, p: &Value) -> Result { @@ -722,7 +722,7 @@ fn connect(s: &mut Session, p: &Value) -> Result { if a.is_empty() || b.is_empty() { return Err(bad("shape.connect", "both shapes need connection sites (lines and groups have none)")); } - let pick = |v: &[slidecraft_geom::Point], key: &str| usize_param(p, key).filter(|i| *i < v.len()); + let pick = |v: &[deckcraft_geom::Point], key: &str| usize_param(p, key).filter(|i| *i < v.len()); let (ia, ib) = match (pick(&a, "fromSite"), pick(&b, "toSite")) { (Some(i), Some(j)) => (i, j), (fi, fj) => { @@ -744,24 +744,24 @@ fn connect(s: &mut Session, p: &Value) -> Result { None => { let preset = str_param(p, "preset").unwrap_or("straightConnector1"); let r = s.execute("shape.insert", &json!({"preset": preset, "rect": [p0.x, p0.y, 1.0, 1.0]}))?; - slidecraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.connect", "insert failed"))? as u32) + deckcraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.connect", "insert failed"))? as u32) } }; s.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.connect", "no slide"))?; - let sh = slidecraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.connect", "no such line"))?; + let sh = deckcraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.connect", "no such line"))?; if !sh.is_line() { return Err(bad("shape.connect", "`id` is not a line or connector")); } sh.xfrm = Some(crate::tools::line_xfrm(p0, p1, 0.0)); - sh.kind = slidecraft_model::ShapeKind::Connector { start: Some((from, ia as u32)), end: Some((to, ib as u32)) }; + sh.kind = deckcraft_model::ShapeKind::Connector { start: Some((from, ia as u32)), end: Some((to, ib as u32)) }; Ok(()) })?; Ok(json!({"id": id.0, "fromSite": ia, "toSite": ib})) } /// Path data through `pts` (shape-local): straight segments, or a Catmull-Rom curve as cubics. -pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool) -> String { +pub fn freeform_path(pts: &[deckcraft_geom::Point], closed: bool, smooth: bool) -> String { let f = |v: f64| { let s = format!("{:.2}", v); s.trim_end_matches('0').trim_end_matches('.').to_string() @@ -769,7 +769,7 @@ pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool) let mut d = format!("M {} {}", f(pts[0].x), f(pts[0].y)); let n = pts.len(); if smooth && n > 2 { - let at = |i: isize| -> slidecraft_geom::Point { + let at = |i: isize| -> deckcraft_geom::Point { if closed { pts[i.rem_euclid(n as isize) as usize] } else { pts[i.clamp(0, n as isize - 1) as usize] } }; let segs = if closed { n } else { n - 1 }; @@ -791,12 +791,12 @@ pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool) } fn freeform(s: &mut Session, p: &Value) -> Result { - let pts: Vec = p + let pts: Vec = p .get("points") .and_then(Value::as_array) .map(|a| { a.iter() - .filter_map(|v| Some(slidecraft_geom::Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?))) + .filter_map(|v| Some(deckcraft_geom::Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?))) .filter(|q| q.x.is_finite() && q.y.is_finite()) .take(20_000) .collect() @@ -807,30 +807,30 @@ fn freeform(s: &mut Session, p: &Value) -> Result { } let closed = bool_or(p, "closed", false) && pts.len() > 2; let smooth = bool_or(p, "smooth", false); - let mut bb = slidecraft_geom::Rect::from_points(pts[0], pts[0]); + let mut bb = deckcraft_geom::Rect::from_points(pts[0], pts[0]); for q in &pts { bb = bb.union_pt(*q); } let (w, h) = (bb.width().max(1.0), bb.height().max(1.0)); - let local: Vec = pts.iter().map(|q| slidecraft_geom::Point::new(q.x - bb.x0, q.y - bb.y0)).collect(); + let local: Vec = pts.iter().map(|q| deckcraft_geom::Point::new(q.x - bb.x0, q.y - bb.y0)).collect(); let d = freeform_path(&local, closed, smooth); // Closed shapes take the default shape style, open paths the line style. let r = s.execute("shape.insert", &json!({"preset": if closed { "rect" } else { "line" }, "rect": [bb.x0, bb.y0, w, h]}))?; - let id = slidecraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.freeform", "insert failed"))? as u32); + let id = deckcraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.freeform", "insert failed"))? as u32); s.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.freeform", "no slide"))?; - let sh = slidecraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.freeform", "insert failed"))?; + let sh = deckcraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.freeform", "insert failed"))?; sh.geom = Geom::Custom { - paths: vec![slidecraft_model::CustomPath { + paths: vec![deckcraft_model::CustomPath { w, h, d: d.clone(), - fill: if closed { slidecraft_geom::preset::FillMode::Norm } else { slidecraft_geom::preset::FillMode::None }, + fill: if closed { deckcraft_geom::preset::FillMode::Norm } else { deckcraft_geom::preset::FillMode::None }, stroke: true, }], }; - sh.xfrm = Some(slidecraft_geom::Xfrm::new(bb.x0, bb.y0, w, h)); - sh.kind = slidecraft_model::ShapeKind::Shape; + sh.xfrm = Some(deckcraft_geom::Xfrm::new(bb.x0, bb.y0, w, h)); + sh.kind = deckcraft_model::ShapeKind::Shape; sh.name = format!("Freeform {}", id.0); Ok(()) })?; diff --git a/crates/engine/src/cmd/slide.rs b/crates/engine/src/cmd/slide.rs index a706262..3d8ad34 100644 --- a/crates/engine/src/cmd/slide.rs +++ b/crates/engine/src/cmd/slide.rs @@ -2,9 +2,9 @@ use std::sync::Arc; +use deckcraft_model::text::TextBody; +use deckcraft_model::{LayoutId, LayoutType, Presentation, Section, SlideId, defaults}; use serde_json::{Value, json}; -use slidecraft_model::text::TextBody; -use slidecraft_model::{LayoutId, LayoutType, Presentation, Section, SlideId, defaults}; use super::*; use crate::{Result, Session, Target}; @@ -173,12 +173,12 @@ fn duplicate(s: &mut Session, p: &Value) -> Result { .get(*i) .map(|x| { let mut v = vec![]; - slidecraft_model::walk(&x.shapes, &mut |sh, _| v.push(sh.id)); + deckcraft_model::walk(&x.shapes, &mut |sh, _| v.push(sh.id)); v }) .unwrap_or_default(); let mut new_shape_ids = vec![]; - slidecraft_model::walk(©.shapes, &mut |sh, _| new_shape_ids.push(sh.id)); + deckcraft_model::walk(©.shapes, &mut |sh, _| new_shape_ids.push(sh.id)); for a in &mut copy.animations { if let Some(pos) = old_ids.iter().position(|x| *x == a.shape) { a.shape = new_shape_ids.get(pos).copied().unwrap_or(a.shape); @@ -360,16 +360,16 @@ fn hide(s: &mut Session, p: &Value) -> Result { /// Re-map a slide's placeholders onto a new layout (content kept, matched by type/index). pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> Result<()> { - let lay_shapes: Vec = + let lay_shapes: Vec = doc.layout(layout).map(|(_, l)| l.shapes.clone()).ok_or_else(|| bad("slide.layout", "no such layout"))?; let mut fresh = vec![]; for ls in lay_shapes.iter().filter(|x| x.ph.as_ref().is_some_and(|p| !p.kind.is_footer_kind())) { - let id = slidecraft_model::ShapeId(doc.alloc_id()); + let id = deckcraft_model::ShapeId(doc.alloc_id()); fresh.push((id, ls.clone())); } let slide = doc.slides.get_mut(index).ok_or_else(|| bad("slide.layout", "no slide"))?; let slide = Arc::make_mut(slide); - let mut old: Vec = std::mem::take(&mut slide.shapes); + let mut old: Vec = std::mem::take(&mut slide.shapes); let mut out = vec![]; for (id, ls) in fresh { let Some(lph) = ls.ph.clone() else { continue }; @@ -389,13 +389,13 @@ pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> R out.push(o); } None => { - out.push(slidecraft_model::Shape { id, name: ls.name.clone(), ph: Some(lph), text: Some(TextBody::default()), ..Default::default() }) + out.push(deckcraft_model::Shape { id, name: ls.name.clone(), ph: Some(lph), text: Some(TextBody::default()), ..Default::default() }) } } } // Leftover placeholders with content stay as they were (positioned); empty ones go. for o in old { - let empty_ph = o.ph.is_some() && o.text.as_ref().is_none_or(|t| t.is_empty()) && matches!(o.kind, slidecraft_model::ShapeKind::Shape); + let empty_ph = o.ph.is_some() && o.text.as_ref().is_none_or(|t| t.is_empty()) && matches!(o.kind, deckcraft_model::ShapeKind::Shape); if !empty_ph { out.push(o); } @@ -574,7 +574,7 @@ fn from_outline(s: &mut Session, p: &Value) -> Result { let text = str_param(p, "text").unwrap_or("").to_string(); s.edit(|doc, sel| { let before = doc.slides.len(); - let n = slidecraft_format::outline_to_slides(doc, &text); + let n = deckcraft_format::outline_to_slides(doc, &text); // outline_to_slides appends; move them after the current slide. let at = (sel.slide + 1).min(before); let new: Vec<_> = doc.slides.drain(before..).collect(); diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs index fb8bcfa..e9f6f16 100644 --- a/crates/engine/src/cmd/table.rs +++ b/crates/engine/src/cmd/table.rs @@ -1,7 +1,7 @@ //! Table Design and Table Layout tabs. +use deckcraft_model::{ShapeKind, Table}; use serde_json::{Value, json}; -use slidecraft_model::{ShapeKind, Table}; use super::*; use crate::{Result, Session}; @@ -108,7 +108,7 @@ fn with_table(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut Table) -> R // The edited cell may have moved out of range. s.select(|doc, sel| { let dims = super::current_shapes(doc, sel) - .and_then(|v| slidecraft_model::find_shape(v, id)) + .and_then(|v| deckcraft_model::find_shape(v, id)) .and_then(|x| if let ShapeKind::Table(t) = &x.kind { Some((t.n_rows(), t.n_cols())) } else { None }); if let (Some(t), Some((nr, nc))) = (sel.text.as_mut(), dims) && let Some((r, c)) = t.cell @@ -196,8 +196,7 @@ fn options(s: &mut Session, p: &Value) -> Result { } fn cell_fill(s: &mut Session, p: &Value) -> Result { - let fill = - if bool_or(p, "none", false) { Some(slidecraft_model::Fill::None) } else { color_param(p, "color").map(slidecraft_model::Fill::solid) }; + let fill = if bool_or(p, "none", false) { Some(deckcraft_model::Fill::None) } else { color_param(p, "color").map(deckcraft_model::Fill::solid) }; let cells: Option> = p .get("cells") .and_then(Value::as_array) diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 751251b..a35f55e 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -2,11 +2,11 @@ use std::sync::Arc; +use deckcraft_model::edit::{self as ed, Pos}; +use deckcraft_model::resolve::{self, Ctx}; +use deckcraft_model::text::{AutoFit, Run, RunKind, RunProps, TextBody}; +use deckcraft_model::{Presentation, ShapeId, ShapeKind}; use serde_json::{Value, json}; -use slidecraft_model::edit::{self as ed, Pos}; -use slidecraft_model::resolve::{self, Ctx}; -use slidecraft_model::text::{AutoFit, Run, RunKind, RunProps, TextBody}; -use slidecraft_model::{Presentation, ShapeId, ShapeKind}; use super::*; use crate::{DocState, Result, Selection, Session, Target, TextSel}; @@ -52,7 +52,7 @@ fn body_mut<'a>(doc: &'a mut Presentation, sel: &Selection, t: &TextSel) -> Opti return doc.slides.get_mut(sel.slide).map(|s| &mut Arc::make_mut(s).notes); } let shapes = crate::shapes_mut(doc, sel)?; - let sh = slidecraft_model::find_shape_mut(shapes, t.shape)?; + let sh = deckcraft_model::find_shape_mut(shapes, t.shape)?; if let Some((r, c)) = t.cell { if let ShapeKind::Table(tb) = &mut sh.kind { return tb.cell_mut(r, c).map(|x| &mut x.text); @@ -92,14 +92,14 @@ pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> { return Ok(()); } let x = xfrm_of(&doc, &sel, &sh); - let geo = slidecraft_render::shape_geometry(&sh, x.w, x.h); - let fields = slidecraft_text::NoFields; + let geo = deckcraft_render::shape_geometry(&sh, x.w, x.h); + let fields = deckcraft_text::NoFields; let mut nx = x; if bp.wrap == Some(false) { - nx.w = slidecraft_text::fit_width(&ctx, &sh, &body, geo.text_rect, &fields).max(20.0); + nx.w = deckcraft_text::fit_width(&ctx, &sh, &body, geo.text_rect, &fields).max(20.0); } - let geo = slidecraft_render::shape_geometry(&sh, nx.w, nx.h); - let need = slidecraft_text::fit_height(&ctx, &sh, &body, geo.text_rect, &fields); + let geo = deckcraft_render::shape_geometry(&sh, nx.w, nx.h); + let need = deckcraft_text::fit_height(&ctx, &sh, &body, geo.text_rect, &fields); let extra = (nx.h - geo.text_rect.height()).max(0.0); nx.h = (need + extra).max(8.0); if (nx.h - x.h).abs() < 0.01 && (nx.w - x.w).abs() < 0.01 && sh.xfrm.is_some() { @@ -109,7 +109,7 @@ pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> { let st = s.doc_mut()?; let mut d = (*st.doc).clone(); if let Some(list) = crate::shapes_mut(&mut d, &st.selection) - && let Some(target) = slidecraft_model::find_shape_mut(list, id) + && let Some(target) = deckcraft_model::find_shape_mut(list, id) { target.xfrm = Some(nx); } @@ -127,7 +127,7 @@ pub(crate) fn ctx_for<'a>(doc: &'a Presentation, sel: &Selection) -> Option Option { +pub fn layout_for(st: &DocState, t: &TextSel) -> Option { if t.notes || t.cell.is_some() { return None; } @@ -135,12 +135,12 @@ pub fn layout_for(st: &DocState, t: &TextSel) -> Option Result { if sh.text.is_none() && cell.is_none() { s.edit(|doc, sel| { if let Some(list) = crate::shapes_mut(doc, sel) - && let Some(x) = slidecraft_model::find_shape_mut(list, id) + && let Some(x) = deckcraft_model::find_shape_mut(list, id) { x.text = Some(TextBody::default()); } @@ -313,7 +313,7 @@ pub(crate) fn insert_field(s: &mut Session, field: &str, text: &str) -> Result Result { let t = st.selection.text.clone().ok_or_else(|| bad("text.move", "not editing text"))?; let body = body_of(st, &t).cloned().unwrap_or_default(); let layout = layout_for(st, &t); - let lp = |c: Pos| slidecraft_text::Pos { para: c.0, ch: c.1 }; + let lp = |c: Pos| deckcraft_text::Pos { para: c.0, ch: c.1 }; let caret = t.caret; let collapse_to = |left: bool| if left { t.ordered().0 } else { t.ordered().1 }; let new = match to.as_str() { @@ -490,7 +490,7 @@ fn set(s: &mut Session, p: &Value) -> Result { }; let Some(body) = target else { return Err(bad("text.set", "no text here (tables need `cell`)")) }; let keep = body.paragraphs.first().and_then(|p| p.runs.first()).map(|r| r.props.clone()).unwrap_or_default(); - let levels: Vec<(u8, slidecraft_model::text::ParaProps)> = body.paragraphs.iter().map(|p| (p.level, p.props.clone())).collect(); + let levels: Vec<(u8, deckcraft_model::text::ParaProps)> = body.paragraphs.iter().map(|p| (p.level, p.props.clone())).collect(); let mut nb = TextBody::from_text(&text); for (i, para) in nb.paragraphs.iter_mut().enumerate() { // Leading tabs set the outline level. @@ -593,7 +593,7 @@ pub(crate) fn format_run(s: &mut Session, f: &(dyn Fn(&mut RunProps) + Send + Sy /// Apply paragraph changes to the paragraphs touched by the selection (or all paragraphs of the /// selected shapes). -pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::text::Paragraph) + Send + Sync)) -> Result { +pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::text::Paragraph) + Send + Sync)) -> Result { let st = s.doc()?; if let Some(t) = st.selection.text.clone() { let (a, b) = t.ordered(); @@ -639,7 +639,7 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::te } /// Apply body (text box) changes to the edited or selected shapes. -pub(crate) fn format_body(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::text::BodyProps) + Send + Sync)) -> Result { +pub(crate) fn format_body(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::text::BodyProps) + Send + Sync)) -> Result { let st = s.doc()?; let ids: Vec = match &st.selection.text { Some(t) if !t.notes => vec![t.shape], diff --git a/crates/engine/src/cmd/transition.rs b/crates/engine/src/cmd/transition.rs index b7ea2e5..616cf00 100644 --- a/crates/engine/src/cmd/transition.rs +++ b/crates/engine/src/cmd/transition.rs @@ -2,9 +2,9 @@ use std::sync::Arc; +use deckcraft_model::Transition; +use deckcraft_model::anim::TRANSITIONS; use serde_json::{Value, json}; -use slidecraft_model::Transition; -use slidecraft_model::anim::TRANSITIONS; use super::*; use crate::{Result, Session}; diff --git a/crates/engine/src/connect.rs b/crates/engine/src/connect.rs index 87b325a..8cdbecd 100644 --- a/crates/engine/src/connect.rs +++ b/crates/engine/src/connect.rs @@ -1,8 +1,8 @@ //! Connectors glued to shapes: connection sites, snapping line ends to them, and rerouting attached //! connectors whenever the shapes they connect move, resize or rotate. -use slidecraft_geom::{Point, Xfrm}; -use slidecraft_model::{Presentation, Shape, ShapeId, ShapeKind}; +use deckcraft_geom::{Point, Xfrm}; +use deckcraft_model::{Presentation, Shape, ShapeId, ShapeKind}; use crate::Selection; use crate::cmd::xfrm_of; @@ -13,7 +13,7 @@ pub fn sites(doc: &Presentation, sel: &Selection, s: &Shape) -> Vec { return vec![]; } let x = xfrm_of(doc, sel, s); - let mut local = slidecraft_render::shape_geometry(s, x.w, x.h).sites; + let mut local = deckcraft_render::shape_geometry(s, x.w, x.h).sites; if local.is_empty() { local = vec![Point::new(x.w / 2.0, 0.0), Point::new(0.0, x.h / 2.0), Point::new(x.w / 2.0, x.h), Point::new(x.w, x.h / 2.0)]; } diff --git a/crates/engine/src/keys.rs b/crates/engine/src/keys.rs index 41f323c..966a109 100644 --- a/crates/engine/src/keys.rs +++ b/crates/engine/src/keys.rs @@ -176,7 +176,7 @@ impl Session { let st = self.doc()?; let Some(t) = st.selection.text.clone() else { return Ok(Value::Null) }; let Some((r, c)) = t.cell else { return Ok(Value::Null) }; - let Some(slidecraft_model::ShapeKind::Table(tb)) = st.shape(t.shape).map(|s| s.kind.clone()) else { return Ok(Value::Null) }; + let Some(deckcraft_model::ShapeKind::Table(tb)) = st.shape(t.shape).map(|s| s.kind.clone()) else { return Ok(Value::Null) }; let (nr, nc) = (tb.n_rows(), tb.n_cols()); let idx = r * nc + c; if !back && idx + 1 >= nr * nc { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 8f9d261..a7d47aa 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1,4 +1,4 @@ -//! The SlideCraft engine façade. +//! The DeckCraft engine façade. //! //! Every user-visible action is a command with a stable id (`slide.new`, `shape.insert`, //! `text.insert`, `format.bold`…) and JSON parameters. The egui UI, the CLI, the control channel @@ -18,15 +18,15 @@ pub mod tools; use std::sync::Arc; +use deckcraft_model::{Presentation, Shape, ShapeId, Slide, SlideId}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use slidecraft_model::{Presentation, Shape, ShapeId, Slide, SlideId}; pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command}; -pub use slidecraft_geom as geom; -pub use slidecraft_media as media; -pub use slidecraft_model as model; -pub use slidecraft_render as render; +pub use deckcraft_geom as geom; +pub use deckcraft_media as media; +pub use deckcraft_model as model; +pub use deckcraft_render as render; pub use tools::{Mods, PointerEvent, PointerKind, ToolKind}; #[derive(Debug, thiserror::Error)] @@ -45,8 +45,8 @@ pub enum EngineError { Internal(String, String), } -impl From for EngineError { - fn from(e: slidecraft_model::ModelError) -> Self { +impl From for EngineError { + fn from(e: deckcraft_model::ModelError) -> Self { EngineError::Other(e.to_string()) } } @@ -184,7 +184,7 @@ impl DocState { } } pub fn shape(&self, id: ShapeId) -> Option<&Shape> { - slidecraft_model::find_shape(self.shapes(), id) + deckcraft_model::find_shape(self.shapes(), id) } pub fn selected_shapes(&self) -> Vec<&Shape> { self.selection.shapes.iter().filter_map(|id| self.shape(*id)).collect() @@ -282,8 +282,8 @@ pub struct Clipboard { pub shapes: Vec, pub slides: Vec, /// Media the copied shapes/slides reference. - pub media: Vec, - pub text: Option, + pub media: Vec, + pub text: Option, pub plain: String, /// How many times the shapes were pasted (each paste offsets further). pub pastes: u32, diff --git a/crates/engine/src/links.rs b/crates/engine/src/links.rs index 0dc6a15..ac0c787 100644 --- a/crates/engine/src/links.rs +++ b/crates/engine/src/links.rs @@ -3,6 +3,6 @@ pub const DISCORD: &str = "https://discord.gg/artcraft"; pub const WEBSITE: &str = "https://getartcraft.com/"; pub const APPS: &str = "https://getartcraft.com/apps"; -pub const APP_PAGE: &str = "https://getartcraft.com/apps/slidecraft"; -pub const GITHUB: &str = "https://github.com/storytold/slidecraft"; -pub const ISSUES: &str = "https://github.com/storytold/slidecraft/issues"; +pub const APP_PAGE: &str = "https://getartcraft.com/apps/deckcraft"; +pub const GITHUB: &str = "https://github.com/storytold/deckcraft"; +pub const ISSUES: &str = "https://github.com/storytold/deckcraft/issues"; diff --git a/crates/engine/src/recovery.rs b/crates/engine/src/recovery.rs index ba6e3c9..30daa81 100644 --- a/crates/engine/src/recovery.rs +++ b/crates/engine/src/recovery.rs @@ -4,7 +4,7 @@ //! `file.recovery.open` reopens what was left there as unsaved presentations that remember where //! they were saved. //! -//! An entry is `.slidecraft` (the presentation) plus `.json` (`{"path", "title", +//! An entry is `.deckcraft` (the presentation) plus `.json` (`{"path", "title", //! "saved"}`: the original file, the title and when it was written, Unix seconds). use std::path::{Path, PathBuf}; @@ -17,16 +17,16 @@ use crate::{DocState, EngineError, Result, Session}; pub fn default_dir() -> Option { let home = std::env::var_os("HOME").map(PathBuf::from); if cfg!(target_os = "macos") { - return home.map(|h| h.join("Library/Application Support/SlideCraft/Recovery")); + return home.map(|h| h.join("Library/Application Support/DeckCraft/Recovery")); } if cfg!(target_os = "windows") { - return std::env::var_os("APPDATA").map(|a| PathBuf::from(a).join("SlideCraft").join("Recovery")); + return std::env::var_os("APPDATA").map(|a| PathBuf::from(a).join("DeckCraft").join("Recovery")); } - std::env::var_os("XDG_DATA_HOME").map(PathBuf::from).or_else(|| home.map(|h| h.join(".local/share"))).map(|d| d.join("slidecraft/recovery")) + std::env::var_os("XDG_DATA_HOME").map(PathBuf::from).or_else(|| home.map(|h| h.join(".local/share"))).map(|d| d.join("deckcraft/recovery")) } fn files(dir: &Path, uid: u64) -> (PathBuf, PathBuf) { - (dir.join(format!("{uid}.slidecraft")), dir.join(format!("{uid}.json"))) + (dir.join(format!("{uid}.deckcraft")), dir.join(format!("{uid}.json"))) } fn now_secs() -> u64 { @@ -58,7 +58,7 @@ pub fn save(s: &Session, dir: &Path) -> Result { continue; } let (doc, meta) = files(dir, d.uid); - let bytes = slidecraft_format::save(&d.doc).map_err(|e| EngineError::Other(e.to_string()))?; + let bytes = deckcraft_format::save(&d.doc).map_err(|e| EngineError::Other(e.to_string()))?; // Write then rename, so a crash mid-write never leaves a torn file. let tmp = doc.with_extension("tmp"); std::fs::write(&tmp, &bytes).and_then(|_| std::fs::rename(&tmp, &doc)).map_err(|e| EngineError::Other(format!("{}: {e}", doc.display())))?; @@ -75,7 +75,7 @@ pub fn list(dir: &Path) -> Vec<(u64, Value)> { let Ok(rd) = std::fs::read_dir(dir) else { return out }; for e in rd.flatten() { let p = e.path(); - if p.extension().and_then(|x| x.to_str()) != Some("slidecraft") { + if p.extension().and_then(|x| x.to_str()) != Some("deckcraft") { continue; } let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::().ok()) else { continue }; @@ -98,8 +98,8 @@ pub fn open(s: &mut Session, dir: &Path) -> Result> { for (uid, meta) in list(dir) { let (doc, _) = files(dir, uid); let Ok(bytes) = std::fs::read(&doc) else { continue }; - let Ok(mut d) = slidecraft_format::load(&bytes) else { continue }; - slidecraft_format::repair(&mut d); + let Ok(mut d) = deckcraft_format::load(&bytes) else { continue }; + deckcraft_format::repair(&mut d); let path = meta.get("path").and_then(Value::as_str).map(str::to_string); let title = meta.get("title").and_then(Value::as_str).filter(|t| !t.is_empty()).unwrap_or("Presentation"); let mut st = DocState::new(d, path, format!("{title} (Recovered)")); @@ -117,7 +117,7 @@ mod tests { use super::*; fn temp(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!("slidecraft-{name}-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!("deckcraft-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); dir } @@ -143,7 +143,7 @@ mod tests { // Saving drops the entry. s.recovery_dir = Some(dir.clone()); assert_eq!(save(&s, &dir).unwrap(), 1); - let path = dir.join("saved.slidecraft"); + let path = dir.join("saved.deckcraft"); s.execute("file.saveAs", &json!({"path": path.to_string_lossy()})).unwrap(); assert!(list(&dir).is_empty()); let _ = std::fs::remove_dir_all(&dir); @@ -153,7 +153,7 @@ mod tests { fn corrupt_entries_are_skipped() { let dir = temp("recovery-corrupt"); std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join("7.slidecraft"), b"not a presentation").unwrap(); + std::fs::write(dir.join("7.deckcraft"), b"not a presentation").unwrap(); std::fs::write(dir.join("7.json"), b"[1, 2]").unwrap(); let mut s = Session::new(); assert!(open(&mut s, &dir).unwrap().is_empty()); diff --git a/crates/engine/src/sample.rs b/crates/engine/src/sample.rs index 886ffaa..80c11ca 100644 --- a/crates/engine/src/sample.rs +++ b/crates/engine/src/sample.rs @@ -1,5 +1,5 @@ //! A sample deck built entirely in code (our own text, shapes and data), for demos, screenshots -//! and tests: `slidecraft --sample`. +//! and tests: `deckcraft --sample`. use serde_json::{Value, json}; @@ -15,13 +15,13 @@ fn last_id(v: &Value) -> u64 { /// Open the sample presentation in a new document. pub fn open_sample(s: &mut Session) -> Result<()> { - run(s, "file.new", json!({"theme": "SlideCraft"}))?; + run(s, "file.new", json!({"theme": "DeckCraft"}))?; let st = s.doc()?; let title = st.current_slide().and_then(|x| x.shapes.first()).map(|x| x.id.0).unwrap_or(0); let sub = st.current_slide().and_then(|x| x.shapes.get(1)).map(|x| x.id.0).unwrap_or(0); run(s, "text.set", json!({"id": title, "text": "Northern Lights"}))?; run(s, "text.set", json!({"id": sub, "text": "A short tour of the aurora — where it comes from, when to look, and how to photograph it"}))?; - run(s, "slide.notes", json!({"text": "Welcome everyone. This deck was made in SlideCraft."}))?; + run(s, "slide.notes", json!({"text": "Welcome everyone. This deck was made in DeckCraft."}))?; // Decorative bands on the title slide. let b = run(s, "shape.insert", json!({"preset": "rect", "rect": [0, 470, 960, 70]}))?; run(s, "shape.fill", json!({"id": last_id(&b), "gradient": {"stops": [[0, "accent1"], [0.5, "accent4"], [1, "accent2"]], "angle": 0}}))?; @@ -114,7 +114,7 @@ pub fn open_sample(s: &mut Session) -> Result<()> { run(s, "transition.set", json!({"kind": "split"}))?; // Closing. - run(s, "slide.new", json!({"layout": "title", "title": "Clear skies!", "body": "Made with SlideCraft — free and open source"}))?; + run(s, "slide.new", json!({"layout": "title", "title": "Clear skies!", "body": "Made with DeckCraft — free and open source"}))?; let moon = run(s, "shape.insert", json!({"preset": "moon", "rect": [820, 40, 80, 120]}))?; run(s, "shape.fill", json!({"id": last_id(&moon), "color": "accent6"}))?; run(s, "shape.line", json!({"id": last_id(&moon), "none": true}))?; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index fb52d08..43ac6c4 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -174,7 +174,7 @@ fn sample_deck_builds_and_renders() { assert!(st.doc.validate().is_empty()); assert!(!st.is_dirty()); for i in 0..st.doc.slides.len() { - let img = slidecraft_render::render_slide(&st.doc, i, &slidecraft_render::RenderOpts { scale: 0.2, ..Default::default() }); + let img = deckcraft_render::render_slide(&st.doc, i, &deckcraft_render::RenderOpts { scale: 0.2, ..Default::default() }); assert_eq!(img.width, 192); } let v = s.execute("document.inspect", &json!({})).unwrap(); @@ -187,7 +187,7 @@ fn save_and_reopen_native() { sample::open_sample(&mut s).unwrap(); let bytes = s.execute("file.saveBytes", &json!({})).unwrap()["data"].as_str().unwrap().to_string(); let n = s.doc().unwrap().doc.slides.len(); - s.execute("file.openBytes", &json!({"name": "x.slidecraft", "data": bytes})).unwrap(); + s.execute("file.openBytes", &json!({"name": "x.deckcraft", "data": bytes})).unwrap(); assert_eq!(s.doc().unwrap().doc.slides.len(), n); assert_eq!(s.documents().len(), 2); } @@ -249,9 +249,9 @@ fn themes_and_slide_size() { assert!(v["shapes"].as_array().unwrap().iter().any(|x| x["placeholder"] == "sldNum")); } -fn line_ends(s: &Session, id: u64) -> (slidecraft_geom::Point, slidecraft_geom::Point) { +fn line_ends(s: &Session, id: u64) -> (deckcraft_geom::Point, deckcraft_geom::Point) { let st = s.doc().unwrap(); - let sh = st.shape(slidecraft_model::ShapeId(id as u32)).unwrap(); + let sh = st.shape(deckcraft_model::ShapeId(id as u32)).unwrap(); connect::endpoints(&cmd::xfrm_of(&st.doc, &st.selection, sh)) } @@ -271,7 +271,7 @@ fn connectors_follow_glued_shapes() { s.execute("shape.move", &json!({"dx": 50, "dy": 20})).unwrap(); let (q0, q1) = line_ends(&s, c); assert!((q0 - p0).hypot() < 1e-6); - assert!(((q1 - p1) - slidecraft_geom::Vec2::new(50.0, 20.0)).hypot() < 1e-6, "{p1:?} → {q1:?}"); + assert!(((q1 - p1) - deckcraft_geom::Vec2::new(50.0, 20.0)).hypot() < 1e-6, "{p1:?} → {q1:?}"); // Undo puts both back in one step. s.execute("edit.undo", &json!({})).unwrap(); assert!((line_ends(&s, c).1 - p1).hypot() < 1e-6); @@ -279,8 +279,8 @@ fn connectors_follow_glued_shapes() { s.execute("edit.select", &json!({"ids": [a]})).unwrap(); s.execute("edit.delete", &json!({})).unwrap(); let st = s.doc().unwrap(); - let sh = st.shape(slidecraft_model::ShapeId(c as u32)).unwrap(); - assert!(matches!(sh.kind, slidecraft_model::ShapeKind::Connector { start: None, end: Some(_) })); + let sh = st.shape(deckcraft_model::ShapeId(c as u32)).unwrap(); + assert!(matches!(sh.kind, deckcraft_model::ShapeKind::Connector { start: None, end: Some(_) })); } #[test] @@ -301,7 +301,7 @@ fn drawing_a_line_between_shapes_glues_it() { let st = s.doc().unwrap(); let line = st.shapes().iter().find(|x| x.is_line()).unwrap(); match line.kind { - slidecraft_model::ShapeKind::Connector { start: Some((x, _)), end: Some((y, _)) } => { + deckcraft_model::ShapeKind::Connector { start: Some((x, _)), end: Some((y, _)) } => { assert_eq!((x.0 as u64, y.0 as u64), (a, b)); } ref k => panic!("not glued: {k:?}"), @@ -336,7 +336,7 @@ fn freeform_tools_and_command() { let st = s.doc().unwrap(); let sh = st.shapes().last().unwrap(); match &sh.geom { - slidecraft_model::Geom::Custom { paths } => assert!(paths[0].d.ends_with('Z') && paths[0].d.matches(" L ").count() == 2, "{}", paths[0].d), + deckcraft_model::Geom::Custom { paths } => assert!(paths[0].d.ends_with('Z') && paths[0].d.matches(" L ").count() == 2, "{}", paths[0].d), g => panic!("{g:?}"), } let x = sh.xfrm.unwrap(); @@ -359,10 +359,10 @@ fn freeform_tools_and_command() { s.pointer(ev(tools::PointerKind::Up, x, y)).unwrap(); } let r = s.key("Enter", Default::default()).unwrap(); - let id = slidecraft_model::ShapeId(r["id"].as_u64().unwrap() as u32); + let id = deckcraft_model::ShapeId(r["id"].as_u64().unwrap() as u32); let st = s.doc().unwrap(); match &st.shape(id).unwrap().geom { - slidecraft_model::Geom::Custom { paths } => assert_eq!(paths[0].d.matches(" C ").count(), 3), + deckcraft_model::Geom::Custom { paths } => assert_eq!(paths[0].d.matches(" C ").count(), 3), g => panic!("{g:?}"), } } diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index 0b35fb7..aef394e 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -3,10 +3,10 @@ use std::sync::Arc; +use deckcraft_geom::{Affine, Point, Rect, Vec2, Xfrm}; +use deckcraft_model::{Shape, ShapeId, ShapeKind}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; -use slidecraft_geom::{Affine, Point, Rect, Vec2, Xfrm}; -use slidecraft_model::{Shape, ShapeId, ShapeKind}; use crate::cmd::{self, xfrm_of}; use crate::{HistoryEntry, Result, Session, TextSel}; @@ -55,7 +55,7 @@ pub enum ToolKind { /// Ink: pen (`pen`, `highlighter`) or `eraser`. Ink { mode: String, - color: slidecraft_color::Rgba, + color: deckcraft_color::Rgba, width: f64, }, } @@ -229,7 +229,7 @@ impl Session { } continue; } - let geo = slidecraft_render::shape_geometry(sh, x.w, x.h); + let geo = deckcraft_render::shape_geometry(sh, x.w, x.h); for hd in &geo.handles { if (x.affine() * hd.pos - p).hypot() <= tol * 1.6 { return Some(Hit::Adjust { id: *id, index: hd.index }); @@ -277,7 +277,7 @@ impl Session { return Ok(Value::Null); } self.tool.hover = self.hit_test(p, tol); - let line_tool = matches!(&self.tool.kind, ToolKind::Shape { preset } if slidecraft_geom::preset::is_line_like(preset)); + let line_tool = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset)); if line_tool { self.update_sites(p, tol, None); } else { @@ -485,7 +485,7 @@ impl Session { self.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?; for (id, o) in &origs2 { - if let Some(sh) = slidecraft_model::find_shape_mut(list, *id) { + if let Some(sh) = deckcraft_model::find_shape_mut(list, *id) { sh.xfrm = Some(Xfrm { x: o.x + d.x, y: o.y + d.y, ..*o }); } } @@ -556,7 +556,7 @@ impl Session { let st = self.doc()?; let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) }; let local = orig.affine().inverse() * p; - let geo = slidecraft_render::shape_geometry(&sh, orig.w, orig.h); + let geo = deckcraft_render::shape_geometry(&sh, orig.w, orig.h); let Some(h) = geo.handles.iter().find(|h| h.index == index) else { return Ok(Value::Null) }; let free2d = matches!(sh.geom.preset_name(), Some(n) if n.contains("Callout") && !n.contains("Arrow")); let vals: Vec<(usize, f64)> = if free2d { @@ -568,10 +568,10 @@ impl Session { }; self.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?; - if let Some(sh) = slidecraft_model::find_shape_mut(list, id) - && let slidecraft_model::Geom::Preset { name, adj } = &mut sh.geom + if let Some(sh) = deckcraft_model::find_shape_mut(list, id) + && let deckcraft_model::Geom::Preset { name, adj } = &mut sh.geom { - let defaults = slidecraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]); + let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]); for (i, v) in &vals { while adj.len() <= *i { let k = adj.len(); @@ -589,7 +589,7 @@ impl Session { self.tool.marquee = Some(Rect::from_points(start, p)); } Drag::Create { start } => { - let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if slidecraft_geom::preset::is_line_like(preset)); + let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset)); let x = if line { let mut q = if mods.shift { constrain_45(start, p) } else { p }; if let Some((_, _, at)) = self.update_sites(q, tol, None) { @@ -693,7 +693,7 @@ impl Session { let dragged = (p - start).hypot() > 2.0; let res = match &kind { ToolKind::Shape { preset } => { - let line = slidecraft_geom::preset::is_line_like(preset); + let line = deckcraft_geom::preset::is_line_like(preset); let rect = match preview.filter(|_| dragged) { Some(x) => x, None if line => Xfrm::new(start.x, start.y, 72.0, 0.0), @@ -789,7 +789,7 @@ impl Session { let x = xfrm_of(&st.doc, &st.selection, &sh); let local = x.affine().inverse() * p; if let ShapeKind::Table(t) = &sh.kind { - let cell = slidecraft_render_cell(t, local); + let cell = deckcraft_render_cell(t, local); return self.execute("text.edit", &json!({"id": id, "cell": [cell.0, cell.1]})); } Ok(Value::Null) @@ -857,7 +857,7 @@ impl Session { fn set_xfrm(&mut self, id: ShapeId, x: Xfrm) -> Result<()> { self.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("tool", "no slide"))?; - if let Some(sh) = slidecraft_model::find_shape_mut(list, id) { + if let Some(sh) = deckcraft_model::find_shape_mut(list, id) { sh.xfrm = Some(x); if let ShapeKind::Table(t) = &mut sh.kind { // Tables resize their columns and rows proportionally. @@ -875,7 +875,7 @@ impl Session { self.execute("shape.setBounds", &json!({"id": id, "x": x.x, "y": x.y, "w": x.w, "h": x.h}))?; if x.flip_h || x.flip_v { self.edit(|doc, sel| { - if let Some(sh) = crate::shapes_mut(doc, sel).and_then(|l| slidecraft_model::find_shape_mut(l, id)) + if let Some(sh) = crate::shapes_mut(doc, sel).and_then(|l| deckcraft_model::find_shape_mut(l, id)) && let Some(xx) = sh.xfrm.as_mut() { xx.flip_h = x.flip_h; @@ -969,14 +969,14 @@ impl Session { let rr = r + Vec2::new(d, 0.0); guides.push(Guide { vertical: true, pos: x, from: b.y0.min(rr.y0), to: b.y1.max(rr.y1) }); } else if self.prefs.snap_to_grid { - off.x = slidecraft_geom::snap(r.x0, self.prefs.grid_spacing) - r.x0; + off.x = deckcraft_geom::snap(r.x0, self.prefs.grid_spacing) - r.x0; } if let Some((d, y, b)) = best_y { off.y = d; let rr = r + Vec2::new(0.0, d); guides.push(Guide { vertical: false, pos: y, from: b.x0.min(rr.x0), to: b.x1.max(rr.x1) }); } else if self.prefs.snap_to_grid { - off.y = slidecraft_geom::snap(r.y0, self.prefs.grid_spacing) - r.y0; + off.y = deckcraft_geom::snap(r.y0, self.prefs.grid_spacing) - r.y0; } (off, guides) } @@ -1014,7 +1014,7 @@ impl Session { fn add_ink(&mut self, points: Vec<(f64, f64, f32)>) -> Result { let ToolKind::Ink { mode, color, width } = self.tool.kind.clone() else { return Ok(Value::Null) }; - let stroke = slidecraft_model::InkStroke { points, color, width, highlighter: mode == "highlighter" }; + let stroke = deckcraft_model::InkStroke { points, color, width, highlighter: mode == "highlighter" }; // Strokes join the slide's ink layer (one ink shape per slide). let existing = self.doc()?.shapes().iter().rev().find(|s| matches!(s.kind, ShapeKind::Ink { .. })).map(|s| s.id); self.edit(|doc, sel| { @@ -1035,7 +1035,7 @@ impl Session { } }; let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("ink", "no slide"))?; - if let Some(sh) = slidecraft_model::find_shape_mut(list, id) + if let Some(sh) = deckcraft_model::find_shape_mut(list, id) && let ShapeKind::Ink { strokes } = &mut sh.kind { strokes.push(stroke); @@ -1051,7 +1051,7 @@ impl Session { let sel_before = st.selection.clone(); self.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("ink", "no slide"))?; - if let Some(sh) = slidecraft_model::find_shape_mut(list, ink) + if let Some(sh) = deckcraft_model::find_shape_mut(list, ink) && let ShapeKind::Ink { strokes } = &mut sh.kind { strokes.retain(|s| !s.points.iter().any(|(x, y, _)| (Point::new(*x, *y) - p).hypot() <= tol * 2.0 + s.width)); @@ -1186,7 +1186,7 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option { // A group is hit when any child is hit; the group is what gets selected. let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h); - let m = slidecraft_geom::group_child_affine(&x, ch); + let m = deckcraft_geom::group_child_affine(&x, ch); let q = m.inverse() * p; for c in children.iter().rev() { if let Some(cx) = c.xfrm { @@ -1208,9 +1208,9 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option { if sh.is_line() { - let geo = slidecraft_render::shape_geometry(sh, x.w, x.h); + let geo = deckcraft_render::shape_geometry(sh, x.w, x.h); let lw = sh.line.as_ref().and_then(|l| l.width).unwrap_or(1.0); - if slidecraft_geom::near_path(&geo.outline(), local, tol.max(lw)) { + if deckcraft_geom::near_path(&geo.outline(), local, tol.max(lw)) { return Some(Hit::Shape { id: sh.id, text: false }); } return None; @@ -1221,11 +1221,11 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option Option (usize, usize) { } /// Which table cell contains a table-local point. -fn slidecraft_render_cell(t: &slidecraft_model::Table, p: Point) -> (usize, usize) { +fn deckcraft_render_cell(t: &deckcraft_model::Table, p: Point) -> (usize, usize) { let mut y = 0.0; let mut row = t.rows.len().saturating_sub(1); for (i, r) in t.rows.iter().enumerate() { diff --git a/crates/fonts/Cargo.toml b/crates/fonts/Cargo.toml index 11194a1..64944f0 100644 --- a/crates/fonts/Cargo.toml +++ b/crates/fonts/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "slidecraft-fonts" +name = "deckcraft-fonts" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft fonts: font database (craft-fonts + system), substitutes for Office fonts, metrics, outlines, shaping" +description = "DeckCraft fonts: font database (craft-fonts + system), substitutes for Office fonts, metrics, outlines, shaping" [lints] workspace = true diff --git a/crates/fonts/build.rs b/crates/fonts/build.rs index 0e351e0..76e7dc4 100644 --- a/crates/fonts/build.rs +++ b/crates/fonts/build.rs @@ -7,7 +7,7 @@ use std::fmt::Write as _; use std::path::PathBuf; -/// Families SlideCraft never embeds (AGENTS.md §1: no Adobe-authored visual design). +/// Families DeckCraft never embeds (AGENTS.md §1: no Adobe-authored visual design). const EXCLUDED_FAMILIES: &[&str] = &["Source Sans 3", "Source Serif 4", "Source Han Sans", "Source Han Serif", "Noto Sans CJK SC"]; fn main() { @@ -53,7 +53,7 @@ fn manifest(dir: &std::path::Path, manifest: &std::path::Path) -> Result` files with the embedded media, and `mimetype`. Readers ignore unknown //! fields, so newer files open in older builds with what those builds understand. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -7,11 +7,13 @@ use std::io::{Cursor, Read, Write}; use std::sync::Arc; -use slidecraft_model::text::{Paragraph, TextBody}; -use slidecraft_model::{LayoutType, Presentation, defaults}; +use deckcraft_model::text::{Paragraph, TextBody}; +use deckcraft_model::{LayoutType, Presentation, defaults}; -pub const MIMETYPE: &str = "application/vnd.storyteller.slidecraft+zip"; -pub const EXTENSION: &str = "slidecraft"; +pub const MIMETYPE: &str = "application/vnd.storyteller.deckcraft+zip"; +/// Files written before the SlideCraft → DeckCraft rename (`.slidecraft`) are still read. +pub const LEGACY_MIMETYPE: &str = concat!("application/vnd.storyteller.", "slide", "craft+zip"); +pub const EXTENSION: &str = "deckcraft"; /// Format version written into the manifest. pub const VERSION: u32 = 1; /// Largest entry we will inflate (guards against zip bombs). @@ -19,7 +21,7 @@ const MAX_ENTRY: u64 = 2 << 30; #[derive(Debug, thiserror::Error)] pub enum FormatError { - #[error("not a SlideCraft file: {0}")] + #[error("not a DeckCraft file: {0}")] NotOurs(String), #[error("damaged file: {0}")] Damaged(String), @@ -48,9 +50,9 @@ pub fn save(p: &Presentation) -> Result> { z.start_file("mimetype", stored).map_err(w)?; z.write_all(MIMETYPE.as_bytes()).map_err(|e| FormatError::Write(e.to_string()))?; let m = Manifest { - format: "slidecraft".into(), + format: "deckcraft".into(), version: VERSION, - generator: format!("SlideCraft {}", env!("CARGO_PKG_VERSION")), + generator: format!("DeckCraft {}", env!("CARGO_PKG_VERSION")), presentation: p.clone(), }; let json = serde_json::to_vec(&m).map_err(|e| FormatError::Write(e.to_string()))?; @@ -79,9 +81,9 @@ fn is_compressed(ct: &str) -> bool { || ct.contains("zip") } -/// Does this look like a `.slidecraft` file? +/// Does this look like a `.deckcraft` file? pub fn sniff(bytes: &[u8]) -> bool { - bytes.starts_with(b"PK") && bytes.windows(MIMETYPE.len()).take(200).any(|w| w == MIMETYPE.as_bytes()) + bytes.starts_with(b"PK") && [MIMETYPE, LEGACY_MIMETYPE].iter().any(|m| bytes.windows(m.len()).take(200).any(|w| w == m.as_bytes())) } fn read_entry(z: &mut zip::ZipArchive>, name: &str) -> Result> { @@ -99,7 +101,7 @@ pub fn load(bytes: &[u8]) -> Result { let mut z = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| FormatError::NotOurs(e.to_string()))?; let json = read_entry(&mut z, "presentation.json").map_err(|_| FormatError::NotOurs("no presentation.json".into()))?; let m: Manifest = serde_json::from_slice(&json).map_err(|e| FormatError::Damaged(e.to_string()))?; - if m.format != "slidecraft" { + if m.format != "deckcraft" { return Err(FormatError::NotOurs(m.format)); } let mut p = m.presentation; @@ -126,7 +128,7 @@ pub fn repair(p: &mut Presentation) { let needs_layout = p.slides.get(i).is_some_and(|s| p.layout(s.layout).is_none()); let dup = p.slides.get(i).is_some_and(|s| !seen.insert(s.id)); if needs_layout || dup { - let new_id = if dup { Some(slidecraft_model::SlideId(p.alloc_id())) } else { None }; + let new_id = if dup { Some(deckcraft_model::SlideId(p.alloc_id())) } else { None }; if let Some(s) = p.slides.get_mut(i) { let s = Arc::make_mut(s); if needs_layout { @@ -150,8 +152,8 @@ pub fn repair(p: &mut Presentation) { pub fn outline_to_slides(p: &mut Presentation, text: &str) -> usize { let Some(lay) = defaults::layout_of_kind(p, LayoutType::TitleAndContent) else { return 0 }; let mut made = 0; - let mut cur: Option = None; - let flush = |p: &mut Presentation, s: Option, made: &mut usize| { + let mut cur: Option = None; + let flush = |p: &mut Presentation, s: Option, made: &mut usize| { if let Some(s) = s { p.slides.push(Arc::new(s)); *made += 1; @@ -242,7 +244,7 @@ mod tests { #[test] fn repair_fixes_missing_layout_and_size() { let mut p = Presentation::default(); - Arc::make_mut(&mut p.slides[0]).layout = slidecraft_model::LayoutId(99999); + Arc::make_mut(&mut p.slides[0]).layout = deckcraft_model::LayoutId(99999); p.slide_size.width = f64::NAN; repair(&mut p); assert!(p.validate().is_empty()); diff --git a/crates/geom/Cargo.toml b/crates/geom/Cargo.toml index e261479..b648d9e 100644 --- a/crates/geom/Cargo.toml +++ b/crates/geom/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "slidecraft-geom" +name = "deckcraft-geom" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft geometry: units (EMU/pt), transforms, preset shape generators, adjust handles, hit testing" +description = "DeckCraft geometry: units (EMU/pt), transforms, preset shape generators, adjust handles, hit testing" [lints] workspace = true diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs index 2a0e7d1..7a245d8 100644 --- a/crates/geom/src/lib.rs +++ b/crates/geom/src/lib.rs @@ -1,4 +1,4 @@ -//! SlideCraft geometry: units, shape transforms and preset shape generators. +//! DeckCraft geometry: units, shape transforms and preset shape generators. //! //! The document model keeps positions in points (1/72 inch) as `f64`. Office Open XML stores EMU //! (English Metric Units, 12 700 per point); [`emu_to_pt`] and [`pt_to_emu`] convert losslessly for diff --git a/crates/h264/Cargo.toml b/crates/h264/Cargo.toml index 18ddb56..bb037ab 100644 --- a/crates/h264/Cargo.toml +++ b/crates/h264/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-h264" +name = "deckcraft-h264" description = "Clean-room pure-Rust H.264/AVC decoder (ITU-T H.264)" version.workspace = true edition.workspace = true @@ -14,6 +14,6 @@ default = ["threads"] threads = ["dep:rayon"] [dependencies] -slidecraft-bitstream = { workspace = true } +deckcraft-bitstream = { workspace = true } thiserror = { workspace = true } rayon = { workspace = true, optional = true } diff --git a/crates/h264/src/cavlc.rs b/crates/h264/src/cavlc.rs index f6aa085..9711d25 100644 --- a/crates/h264/src/cavlc.rs +++ b/crates/h264/src/cavlc.rs @@ -2,7 +2,7 @@ use crate::cavlc_tables::*; use crate::error::{Result, ensure}; -use slidecraft_bitstream::BitReader; +use deckcraft_bitstream::BitReader; use std::sync::OnceLock; /// Prefix-code lookup keyed by (number of leading zeros, next 5 bits after the first one bit). @@ -182,7 +182,7 @@ pub(crate) fn residual_block(r: &mut BitReader, coeff: &mut [i32], start_idx: us #[cfg(test)] mod tests { use super::*; - use slidecraft_bitstream::BitWriter; + use deckcraft_bitstream::BitWriter; fn kraft(codes: &[&str]) -> f64 { codes.iter().filter(|c| !c.is_empty()).map(|c| 0.5f64.powi(c.len() as i32)).sum() diff --git a/crates/h264/src/deblock.rs b/crates/h264/src/deblock.rs index fb918ff..ded8396 100644 --- a/crates/h264/src/deblock.rs +++ b/crates/h264/src/deblock.rs @@ -480,7 +480,7 @@ mod tests { mod speed { use super::*; - /// Rough timing of the edge filters (`cargo test --release -p slidecraft-h264 --lib edge_filter_speed -- --ignored --nocapture`). + /// Rough timing of the edge filters (`cargo test --release -p deckcraft-h264 --lib edge_filter_speed -- --ignored --nocapture`). #[test] #[ignore] fn edge_filter_speed() { diff --git a/crates/h264/src/decoder.rs b/crates/h264/src/decoder.rs index 3178c64..5f8941e 100644 --- a/crates/h264/src/decoder.rs +++ b/crates/h264/src/decoder.rs @@ -14,7 +14,7 @@ use crate::slice::{NalHeader, Poc, PocState, SliceHeader, SliceType, nal_type}; use crate::slicedec::{PicState, SliceDecoder}; use crate::transform::LevelScale; use crate::{ColorInfo, Picture}; -use slidecraft_bitstream::{BitReader, annexb_nals, length_prefixed_nals, unescape_rbsp}; +use deckcraft_bitstream::{BitReader, annexb_nals, length_prefixed_nals, unescape_rbsp}; use std::collections::VecDeque; use std::sync::{Arc, Mutex}; diff --git a/crates/h264/src/error.rs b/crates/h264/src/error.rs index f7a37c6..7c996b9 100644 --- a/crates/h264/src/error.rs +++ b/crates/h264/src/error.rs @@ -1,4 +1,4 @@ -use slidecraft_bitstream::BitError; +use deckcraft_bitstream::BitError; /// Errors produced by the decoder. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] diff --git a/crates/h264/src/lib.rs b/crates/h264/src/lib.rs index 0173e9d..a65a18e 100644 --- a/crates/h264/src/lib.rs +++ b/crates/h264/src/lib.rs @@ -5,7 +5,7 @@ //! multiple slices, long-term references and all MMCOs, deblocking. //! //! ```no_run -//! let mut dec = slidecraft_h264::Decoder::new(); +//! let mut dec = deckcraft_h264::Decoder::new(); //! let stream = std::fs::read("video.h264").unwrap(); //! for pic in dec.decode(&stream, 0).unwrap() { //! println!("{}x{} poc {}", pic.width, pic.height, pic.poc); diff --git a/crates/h264/src/params.rs b/crates/h264/src/params.rs index bc5db10..f6a3a9e 100644 --- a/crates/h264/src/params.rs +++ b/crates/h264/src/params.rs @@ -2,7 +2,7 @@ use crate::error::{Result, ensure, unsupported}; use crate::tables::*; -use slidecraft_bitstream::BitReader; +use deckcraft_bitstream::BitReader; /// Scaling matrices after applying fall-back rules, stored in raster order (weightScale). #[derive(Clone, Debug, PartialEq, Eq)] @@ -665,7 +665,7 @@ impl Pps { #[cfg(test)] mod tests { use super::*; - use slidecraft_bitstream::BitWriter; + use deckcraft_bitstream::BitWriter; #[test] fn scaling_list_default_signal() { diff --git a/crates/h264/src/slice.rs b/crates/h264/src/slice.rs index 357d89f..c80cf0e 100644 --- a/crates/h264/src/slice.rs +++ b/crates/h264/src/slice.rs @@ -2,7 +2,7 @@ use crate::error::{Result, ensure}; use crate::params::{Pps, Sps}; -use slidecraft_bitstream::BitReader; +use deckcraft_bitstream::BitReader; /// NAL unit types used by the decoder. pub mod nal_type { diff --git a/crates/h264/src/slicedec.rs b/crates/h264/src/slicedec.rs index 16bba04..10132d6 100644 --- a/crates/h264/src/slicedec.rs +++ b/crates/h264/src/slicedec.rs @@ -11,7 +11,7 @@ use crate::picture::*; use crate::slice::{SliceHeader, SliceType}; use crate::tables::*; use crate::transform::{self, LevelScale}; -use slidecraft_bitstream::BitReader; +use deckcraft_bitstream::BitReader; /// Per-slice data kept with the picture (deblocking, motion field export). #[derive(Clone, Debug, Default)] diff --git a/crates/h264/src/synth_tests.rs b/crates/h264/src/synth_tests.rs index fba3cd5..b0e0d5d 100644 --- a/crates/h264/src/synth_tests.rs +++ b/crates/h264/src/synth_tests.rs @@ -6,7 +6,7 @@ use crate::Decoder; use crate::cabac::{Cabac, NEXT_STATE, RANGE_TAB_LPS}; -use slidecraft_bitstream::{BitWriter, escape_rbsp}; +use deckcraft_bitstream::{BitWriter, escape_rbsp}; const MB_W: usize = 4; const MB_H: usize = 3; diff --git a/crates/hevc/Cargo.toml b/crates/hevc/Cargo.toml index cf008d4..b9bcdfa 100644 --- a/crates/hevc/Cargo.toml +++ b/crates/hevc/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-hevc" +name = "deckcraft-hevc" description = "Clean-room pure-Rust H.265/HEVC decoder (ITU-T H.265)" version.workspace = true edition.workspace = true @@ -14,6 +14,6 @@ default = ["threads"] threads = ["dep:rayon"] [dependencies] -slidecraft-bitstream = { workspace = true } +deckcraft-bitstream = { workspace = true } thiserror = { workspace = true } rayon = { workspace = true, optional = true } diff --git a/crates/hevc/src/decoder.rs b/crates/hevc/src/decoder.rs index eb0a302..cd5450f 100644 --- a/crates/hevc/src/decoder.rs +++ b/crates/hevc/src/decoder.rs @@ -12,7 +12,7 @@ use crate::picture::{Frame, FrameRef}; use crate::slice::{NalHeader, SliceHeader, nal_type}; use crate::slicedec::{F_INTRA, F_SKIP, PicState, SliceDecoder, SliceJob}; use crate::{ColorInfo, Picture, Plane}; -use slidecraft_bitstream::{annexb_nals, length_prefixed_nals, unescape_rbsp}; +use deckcraft_bitstream::{annexb_nals, length_prefixed_nals, unescape_rbsp}; use std::collections::VecDeque; use std::sync::{Arc, Mutex}; diff --git a/crates/hevc/src/error.rs b/crates/hevc/src/error.rs index f7a37c6..7c996b9 100644 --- a/crates/hevc/src/error.rs +++ b/crates/hevc/src/error.rs @@ -1,4 +1,4 @@ -use slidecraft_bitstream::BitError; +use deckcraft_bitstream::BitError; /// Errors produced by the decoder. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] diff --git a/crates/hevc/src/lib.rs b/crates/hevc/src/lib.rs index 26c5658..19c09a2 100644 --- a/crates/hevc/src/lib.rs +++ b/crates/hevc/src/lib.rs @@ -7,7 +7,7 @@ //! (parsing), dependent slice segments, short- and long-term reference picture sets. //! //! ```no_run -//! let mut dec = slidecraft_hevc::Decoder::new(); +//! let mut dec = deckcraft_hevc::Decoder::new(); //! let stream = std::fs::read("video.h265").unwrap(); //! for pic in dec.decode(&stream, 0).unwrap() { //! println!("{}x{} poc {}", pic.width, pic.height, pic.poc); diff --git a/crates/hevc/src/params.rs b/crates/hevc/src/params.rs index 7a3d1b7..fbcff55 100644 --- a/crates/hevc/src/params.rs +++ b/crates/hevc/src/params.rs @@ -4,7 +4,7 @@ use crate::error::{Result, ensure, unsupported}; use crate::spec_tables::{DEFAULT_SCALING_INTER, DEFAULT_SCALING_INTRA}; use crate::tables::scan_diag; -use slidecraft_bitstream::BitReader; +use deckcraft_bitstream::BitReader; /// General profile / tier / level information. #[derive(Clone, Debug, Default, PartialEq, Eq)] diff --git a/crates/hevc/src/slice.rs b/crates/hevc/src/slice.rs index 976b1ba..6b4c7ae 100644 --- a/crates/hevc/src/slice.rs +++ b/crates/hevc/src/slice.rs @@ -2,7 +2,7 @@ use crate::error::{Result, ensure, invalid}; use crate::params::{Pps, Sps, StRps}; -use slidecraft_bitstream::BitReader; +use deckcraft_bitstream::BitReader; pub mod nal_type { pub const TRAIL_N: u8 = 0; diff --git a/crates/hevc/src/slicedec.rs b/crates/hevc/src/slicedec.rs index f52910b..db5b46d 100644 --- a/crates/hevc/src/slicedec.rs +++ b/crates/hevc/src/slicedec.rs @@ -799,7 +799,7 @@ impl<'a> SliceDecoder<'a> { let (bl, bc) = (self.sps.pcm_bit_depth_luma, self.sps.pcm_bit_depth_chroma); let total_bits = n * n * bl as usize + 2 * (n / 2) * (n / 2) * bc as usize; ensure!(start + total_bits.div_ceil(8) <= data.len(), "PCM samples truncated"); - let mut r = slidecraft_bitstream::BitReader::new(&data[start..]); + let mut r = deckcraft_bitstream::BitReader::new(&data[start..]); let (sy, sc) = (self.pic.bd_y - bl, self.pic.bd_c - bc); let w = self.pic.width; for y in 0..n { diff --git a/crates/hevc/src/synth_tests.rs b/crates/hevc/src/synth_tests.rs index 892b028..2d5b9fc 100644 --- a/crates/hevc/src/synth_tests.rs +++ b/crates/hevc/src/synth_tests.rs @@ -8,7 +8,7 @@ use crate::Decoder; use crate::cabac::{Contexts, NEXT_STATE, RANGE_TAB_LPS, init_contexts}; use crate::spec_tables::*; -use slidecraft_bitstream::{BitWriter, escape_rbsp}; +use deckcraft_bitstream::{BitWriter, escape_rbsp}; const W: usize = 96; const H: usize = 64; diff --git a/crates/isobmff/Cargo.toml b/crates/isobmff/Cargo.toml index eb2dc56..04c339c 100644 --- a/crates/isobmff/Cargo.toml +++ b/crates/isobmff/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-isobmff" +name = "deckcraft-isobmff" description = "Clean-room ISO BMFF (MP4) and QuickTime (MOV) demuxer and muxer" version.workspace = true edition.workspace = true @@ -10,5 +10,5 @@ rust-version.workspace = true workspace = true [dependencies] -slidecraft-bitstream = { workspace = true } +deckcraft-bitstream = { workspace = true } thiserror = { workspace = true } diff --git a/crates/isobmff/src/codec.rs b/crates/isobmff/src/codec.rs index d78a3c8..4f13294 100644 --- a/crates/isobmff/src/codec.rs +++ b/crates/isobmff/src/codec.rs @@ -2,7 +2,7 @@ use crate::bytes::{BoxBuf, Cur, FourCc, boxes}; use crate::error::{Error, Result}; -use slidecraft_bitstream::BitReader; +use deckcraft_bitstream::BitReader; /// One sample description (an entry of `stsd`). #[derive(Clone, Debug, PartialEq)] diff --git a/crates/matroska/Cargo.toml b/crates/matroska/Cargo.toml index 258a21a..48d2c23 100644 --- a/crates/matroska/Cargo.toml +++ b/crates/matroska/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-matroska" +name = "deckcraft-matroska" description = "Clean-room Matroska (MKV) and WebM demuxer and muxer" version.workspace = true edition.workspace = true diff --git a/crates/matroska/src/lib.rs b/crates/matroska/src/lib.rs index 7f2604c..0f7deb1 100644 --- a/crates/matroska/src/lib.rs +++ b/crates/matroska/src/lib.rs @@ -3,7 +3,7 @@ //! //! - [`open`] / [`open_with`] parse the EBML header and the first Segment (Info, Tracks, SeekHead, //! Cues, Chapters, Tags, Attachments) and, by default, scan every Cluster's block headers to build -//! per-track [`Sample`] tables — the same shape as `slidecraft-isobmff`, so callers can treat both +//! per-track [`Sample`] tables — the same shape as `deckcraft-isobmff`, so callers can treat both //! containers alike. Frame data is read on demand with [`MkvFile::read_sample`]. //! - [`Demuxer`] iterates [`Packet`]s in file order (from any [`ByteSource`], `Read + Seek`, or a //! byte slice) and seeks to the keyframe preceding a time, via the index, Cues, or a cluster scan. diff --git a/crates/matroska/src/mux.rs b/crates/matroska/src/mux.rs index d64443c..462c155 100644 --- a/crates/matroska/src/mux.rs +++ b/crates/matroska/src/mux.rs @@ -78,7 +78,7 @@ impl Default for MuxOptions { lacing: LacingMode::None, max_lace_frames: 8, title: None, - writing_app: "slidecraft".into(), + writing_app: "deckcraft".into(), } } } @@ -166,7 +166,7 @@ impl MkvWriter { let info_pos = base + out.len() as u64; let mut info = Vec::new(); el_uint(&mut info, TIMESTAMP_SCALE, opts.timestamp_scale); - el_str(&mut info, MUXING_APP, "slidecraft-matroska"); + el_str(&mut info, MUXING_APP, "deckcraft-matroska"); el_str(&mut info, WRITING_APP, &opts.writing_app); if let Some(t) = &opts.title { el_str(&mut info, TITLE, t); diff --git a/crates/matroska/src/source.rs b/crates/matroska/src/source.rs index e6f056f..41aabe7 100644 --- a/crates/matroska/src/source.rs +++ b/crates/matroska/src/source.rs @@ -6,7 +6,7 @@ use std::sync::Arc; /// Random-access, read-only byte source (file, in-memory buffer, web Blob…). /// -/// Same shape as `slidecraft_isobmff::ByteSource`, so one adapter can serve both demuxers. +/// Same shape as `deckcraft_isobmff::ByteSource`, so one adapter can serve both demuxers. pub trait ByteSource { /// Total length in bytes. fn len(&self) -> u64; diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index 109669b..ab2efad 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "slidecraft-mcp" +name = "deckcraft-mcp" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft MCP server: JSON-RPC 2.0 over stdio, driving a running app or a headless session" +description = "DeckCraft MCP server: JSON-RPC 2.0 over stdio, driving a running app or a headless session" [lints] workspace = true [dependencies] -slidecraft-engine = { workspace = true } -slidecraft-render = { workspace = true } +deckcraft-engine = { workspace = true } +deckcraft-render = { workspace = true } serde_json = { workspace = true } log = { workspace = true } diff --git a/crates/mcp/src/backend.rs b/crates/mcp/src/backend.rs index 8609e94..f368654 100644 --- a/crates/mcp/src/backend.rs +++ b/crates/mcp/src/backend.rs @@ -7,7 +7,7 @@ use std::time::Duration; use serde_json::{Value, json}; /// Something that answers control-channel methods (`engine.execute`, `document.inspect`, -/// `ui.pointer`, `ui.render`, …). See `slidecraft_ui_egui::control` for the full list. +/// `ui.pointer`, `ui.render`, …). See `deckcraft_ui_egui::control` for the full list. pub trait Backend { /// Call one method. `Ok` carries the `result`, `Err` the error message. fn call(&mut self, method: &str, params: Value) -> Result; @@ -17,7 +17,7 @@ pub trait Backend { fn describe(&self) -> String; } -/// A running SlideCraft app, reached through its loopback control port. +/// A running DeckCraft app, reached through its loopback control port. pub struct Remote { addr: String, conn: Option<(BufReader, TcpStream)>, @@ -85,7 +85,7 @@ impl Backend for Remote { self.conn = None; self.roundtrip(&line).map_err(|e| { self.conn = None; - format!("SlideCraft app at {} is not reachable: {e}", self.addr) + format!("DeckCraft app at {} is not reachable: {e}", self.addr) })? } }; @@ -102,6 +102,6 @@ impl Backend for Remote { } fn describe(&self) -> String { - format!("connected to the SlideCraft app at {}", self.addr) + format!("connected to the DeckCraft app at {}", self.addr) } } diff --git a/crates/mcp/src/headless.rs b/crates/mcp/src/headless.rs index f15c319..512e7cc 100644 --- a/crates/mcp/src/headless.rs +++ b/crates/mcp/src/headless.rs @@ -1,7 +1,7 @@ //! An in-process engine session that answers the engine-level control-channel methods itself. +use deckcraft_engine::{Mods, PointerEvent, PointerKind, Session, ToolKind}; use serde_json::{Value, json}; -use slidecraft_engine::{Mods, PointerEvent, PointerKind, Session, ToolKind}; use crate::backend::Backend; @@ -17,8 +17,8 @@ impl Default for Headless { } /// Hint appended to errors for methods that need a window. -pub(crate) const NEEDS_APP: &str = "it needs the desktop app: start `slidecraft --control 7979` and run the MCP server \ -with `slidecraft-cli mcp --connect 7979`"; +pub(crate) const NEEDS_APP: &str = "it needs the desktop app: start `deckcraft --control 7979` and run the MCP server \ +with `deckcraft-cli mcp --connect 7979`"; fn s<'a>(p: &'a Value, k: &str) -> Option<&'a str> { p.get(k).and_then(Value::as_str) @@ -47,11 +47,11 @@ pub fn tool_kind(name: &str, preset: Option<&str>) -> Option { "select" | "selection" | "arrow" => ToolKind::Select, "textBox" | "text" | "textbox" => ToolKind::TextBox, "shape" => ToolKind::Shape { preset: preset.unwrap_or("rect").to_string() }, - "pen" => ToolKind::Ink { mode: "pen".into(), color: slidecraft_engine::model::Rgba::BLACK, width: 2.0 }, - "highlighter" => ToolKind::Ink { mode: "highlighter".into(), color: slidecraft_engine::model::Rgba::rgb(255, 230, 0), width: 10.0 }, - "eraser" => ToolKind::Ink { mode: "eraser".into(), color: slidecraft_engine::model::Rgba::BLACK, width: 2.0 }, - other if slidecraft_engine::geom::preset::info(other).is_some() => ToolKind::Shape { preset: other.to_string() }, - other if slidecraft_engine::tools::is_freeform_tool(other) => ToolKind::Shape { preset: other.to_string() }, + "pen" => ToolKind::Ink { mode: "pen".into(), color: deckcraft_engine::model::Rgba::BLACK, width: 2.0 }, + "highlighter" => ToolKind::Ink { mode: "highlighter".into(), color: deckcraft_engine::model::Rgba::rgb(255, 230, 0), width: 10.0 }, + "eraser" => ToolKind::Ink { mode: "eraser".into(), color: deckcraft_engine::model::Rgba::BLACK, width: 2.0 }, + other if deckcraft_engine::geom::preset::info(other).is_some() => ToolKind::Shape { preset: other.to_string() }, + other if deckcraft_engine::tools::is_freeform_tool(other) => ToolKind::Shape { preset: other.to_string() }, _ => return None, }) } @@ -69,7 +69,7 @@ impl Headless { fn exec(&mut self, id: &str, params: &Value) -> Result { let r = self.session.execute(id, params).map_err(|e| { let ui_only = ["app.", "view.", "window.", "ui."].iter().any(|p| id.starts_with(p)); - if ui_only && slidecraft_engine::find_command(id).is_none() { format!("`{id}` is a UI command; {NEEDS_APP}") } else { e.to_string() } + if ui_only && deckcraft_engine::find_command(id).is_none() { format!("`{id}` is a UI command; {NEEDS_APP}") } else { e.to_string() } }); self.session.ui_requests.clear(); r @@ -101,17 +101,17 @@ impl Headless { return Err(format!("no slide {i}")); } let scale = p.get("scale").and_then(Value::as_f64).unwrap_or(1.0).clamp(0.05, 8.0); - let img = slidecraft_engine::render::render_slide( + let img = deckcraft_engine::render::render_slide( &st.doc, i, - &slidecraft_engine::render::RenderOpts { scale, edit: p.get("edit").and_then(Value::as_bool).unwrap_or(false), ..Default::default() }, + &deckcraft_engine::render::RenderOpts { scale, edit: p.get("edit").and_then(Value::as_bool).unwrap_or(false), ..Default::default() }, ); let png = img.to_png(); if let Some(path) = s(p, "path") { std::fs::write(path, &png).map_err(|e| format!("{path}: {e}"))?; return Ok(json!({"path": path, "width": img.width, "height": img.height})); } - Ok(json!({"png": slidecraft_engine::cmd::base64_encode(&png), "width": img.width, "height": img.height, "slide": i})) + Ok(json!({"png": deckcraft_engine::cmd::base64_encode(&png), "width": img.width, "height": img.height, "slide": i})) } } diff --git a/crates/mcp/src/lib.rs b/crates/mcp/src/lib.rs index 08123f8..b51ff31 100644 --- a/crates/mcp/src/lib.rs +++ b/crates/mcp/src/lib.rs @@ -1,14 +1,14 @@ -//! SlideCraft's MCP server. +//! DeckCraft's MCP server. //! //! [Model Context Protocol](https://modelcontextprotocol.io) over stdio: newline-delimited -//! JSON-RPC 2.0, hand-written (no async runtime). The server exposes SlideCraft as a set of MCP +//! JSON-RPC 2.0, hand-written (no async runtime). The server exposes DeckCraft as a set of MCP //! tools and resources and forwards everything to a [`Backend`]: //! //! - [`Remote`] talks to a running desktop app through its loopback JSON-lines control channel -//! (`slidecraft --control 7979`): one `{"id","method","params"}` line in, one +//! (`deckcraft --control 7979`): one `{"id","method","params"}` line in, one //! `{"id","ok","result"|"error"}` line out (see `docs/control-protocol.md`). -//! - [`Headless`] hosts an in-process [`slidecraft_engine::Session`] and implements the -//! engine-level control-channel methods itself (rendering pages with `slidecraft-render`), so +//! - [`Headless`] hosts an in-process [`deckcraft_engine::Session`] and implements the +//! engine-level control-channel methods itself (rendering pages with `deckcraft-render`), so //! agents can build layouts and look at them without a window. //! //! Entry points: [`Server::serve`] (stdio loop) and [`Server::handle_line`] (one message). diff --git a/crates/mcp/src/server.rs b/crates/mcp/src/server.rs index 1e231ff..576e0b4 100644 --- a/crates/mcp/src/server.rs +++ b/crates/mcp/src/server.rs @@ -18,17 +18,17 @@ const INVALID_PARAMS: i64 = -32602; const INTERNAL_ERROR: i64 = -32603; const RESOURCE_NOT_FOUND: i64 = -32002; -const INSTRUCTIONS: &str = "SlideCraft is a presentation app (a PowerPoint clone). Coordinates are points on the \ +const INSTRUCTIONS: &str = "DeckCraft is a presentation app (a PowerPoint clone). Coordinates are points on the \ current slide (y down; the default 16:9 slide is 960×540). Every action is a command: find ids and parameters with \ list_commands and run them with run_command (or several with batch). Typical flow: new_presentation → add_slide \ {layout:\"titleAndContent\", title, body} → add_shape / add_text_box / insert_picture / insert_chart → \ inspect_slide to read back ids, boxes and text → render_slide to look at the result. Shapes are selected with \ run_command edit.select {ids:[…]}; formatting commands (format.*, shape.fill, shape.line…) act on the selection or \ -on `ids`. Save with save_presentation (.slidecraft or .pptx)."; +on `ids`. Save with save_presentation (.deckcraft or .pptx)."; /// Resource URIs. -pub const DOC_URI: &str = "slidecraft://document"; -pub const COMMANDS_URI: &str = "slidecraft://commands"; +pub const DOC_URI: &str = "deckcraft://document"; +pub const COMMANDS_URI: &str = "deckcraft://commands"; /// An MCP server bound to one backend. pub struct Server { @@ -135,13 +135,13 @@ impl Server { Ok(json!({ "protocolVersion": version, "capabilities": {"tools": {}, "resources": {}}, - "serverInfo": {"name": "slidecraft", "title": "SlideCraft", "version": env!("CARGO_PKG_VERSION"), "websiteUrl": slidecraft_engine::links::APP_PAGE}, + "serverInfo": {"name": "deckcraft", "title": "DeckCraft", "version": env!("CARGO_PKG_VERSION"), "websiteUrl": deckcraft_engine::links::APP_PAGE}, "instructions": format!( "{INSTRUCTIONS} Backend: {}. Community: {} (Discord), {} (app page), {} (source). The `app.links` command returns these links.", self.backend.describe(), - slidecraft_engine::links::DISCORD, - slidecraft_engine::links::APP_PAGE, - slidecraft_engine::links::GITHUB + deckcraft_engine::links::DISCORD, + deckcraft_engine::links::APP_PAGE, + deckcraft_engine::links::GITHUB ), })) } diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index 806fff7..e5a7ef2 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -23,7 +23,7 @@ fn tool(s: &mut Server, name: &str, args: Value) -> Value { fn handshake_and_tool_list() { let mut s = Server::new(Box::new(Headless::new())); let r = call(&mut s, 1, "initialize", json!({"protocolVersion": "2025-06-18"})); - assert_eq!(r["result"]["serverInfo"]["name"], "slidecraft"); + assert_eq!(r["result"]["serverInfo"]["name"], "deckcraft"); let tools = call(&mut s, 2, "tools/list", json!({})); let names: Vec<&str> = tools["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); for n in ["run_command", "render_slide", "add_slide", "add_shape", "pointer", "inspect_slide"] { diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 9cc12ea..abc791b 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -60,7 +60,7 @@ fn tool(name: &str, title: &str, desc: &str, schema: Value, read_only: bool) -> json!({"name": name, "title": title, "description": desc, "inputSchema": schema, "annotations": {"title": title, "readOnlyHint": read_only, "openWorldHint": false}}) } -const APP_ONLY: &str = " Desktop app only (`slidecraft-cli mcp --connect PORT`)."; +const APP_ONLY: &str = " Desktop app only (`deckcraft-cli mcp --connect PORT`)."; pub fn tool_definitions() -> Vec { vec![ @@ -94,21 +94,21 @@ pub fn tool_definitions() -> Vec { tool( "new_presentation", "New presentation", - "Start a new presentation (one title slide). Optional theme: SlideCraft, Harbor, Ember, Meadow, Nocturne, Paper, Coral Reef, Slate.", + "Start a new presentation (one title slide). Optional theme: DeckCraft, Harbor, Ember, Meadow, Nocturne, Paper, Coral Reef, Slate.", obj(json!({"theme": string("Theme name"), "blank": boolean("No first slide")}), &[]), false, ), tool( "open_presentation", "Open presentation", - "Open a .slidecraft or .pptx file by path.", + "Open a .deckcraft or .pptx file by path.", obj(json!({"path": string("File path")}), &["path"]), false, ), tool( "save_presentation", "Save presentation", - "Save the active presentation (.slidecraft or .pptx by extension).", + "Save the active presentation (.deckcraft or .pptx by extension).", obj(json!({"path": string("File path (optional when it was opened from a file)")}), &[]), false, ), @@ -117,7 +117,7 @@ pub fn tool_definitions() -> Vec { "Export", "Export slides: png/jpeg (one slide, or all with all:true), pptx, outline text.", obj( - json!({"path": string("Output path"), "format": string("png|jpeg|pptx|slidecraft|outline"), "slide": int("Slide index for images"), "all": boolean("All slides (images)"), "scale": num("Pixels per point for images (default 2)")}), + json!({"path": string("Output path"), "format": string("png|jpeg|pptx|deckcraft|outline"), "slide": int("Slide index for images"), "all": boolean("All slides (images)"), "scale": num("Pixels per point for images (default 2)")}), &["path"], ), false, @@ -255,7 +255,7 @@ pub fn tool_definitions() -> Vec { obj(json!({"text": string("Text")}), &["text"]), false, ), - tool("screenshot", "Screenshot", &format!("Screenshot of the whole SlideCraft window (UI included).{APP_ONLY}"), obj(json!({}), &[]), true), + tool("screenshot", "Screenshot", &format!("Screenshot of the whole DeckCraft window (UI included).{APP_ONLY}"), obj(json!({}), &[]), true), tool( "call_app", "Call app method", diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 0b57d48..e785f36 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -1,22 +1,22 @@ [package] -name = "slidecraft-media" +name = "deckcraft-media" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft media: probing, audio decoding (MP3, AAC/M4A, WAV, AIFF, FLAC, ALAC, Vorbis, Opus) to f32 PCM, H.264/HEVC/VP9/AV1 video decoding, poster frames, trim/fade/volume and the playback mixer and clock" +description = "DeckCraft media: probing, audio decoding (MP3, AAC/M4A, WAV, AIFF, FLAC, ALAC, Vorbis, Opus) to f32 PCM, H.264/HEVC/VP9/AV1 video decoding, poster frames, trim/fade/volume and the playback mixer and clock" [lints] workspace = true [dependencies] -slidecraft-isobmff = { workspace = true } -slidecraft-matroska = { workspace = true } -slidecraft-ogg = { workspace = true } -slidecraft-opus = { workspace = true } -slidecraft-h264 = { workspace = true } -slidecraft-hevc = { workspace = true } -slidecraft-vp9 = { workspace = true } -slidecraft-av1 = { workspace = true } +deckcraft-isobmff = { workspace = true } +deckcraft-matroska = { workspace = true } +deckcraft-ogg = { workspace = true } +deckcraft-opus = { workspace = true } +deckcraft-h264 = { workspace = true } +deckcraft-hevc = { workspace = true } +deckcraft-vp9 = { workspace = true } +deckcraft-av1 = { workspace = true } thiserror = { workspace = true } log = { workspace = true } image = { workspace = true } diff --git a/crates/media/src/audio.rs b/crates/media/src/audio.rs index 18c81c0..80ad66a 100644 --- a/crates/media/src/audio.rs +++ b/crates/media/src/audio.rs @@ -84,15 +84,15 @@ pub fn decode(bytes: &Bytes) -> Result { /// Ogg Opus with our decoder; `None` when the file holds no Opus stream. fn decode_ogg_opus(bytes: &Bytes) -> Option> { let src: &[u8] = bytes; - let file = slidecraft_ogg::open(src).ok()?; - let s = file.stream_of(slidecraft_ogg::Codec::Opus)?; + let file = deckcraft_ogg::open(src).ok()?; + let s = file.stream_of(deckcraft_ogg::Codec::Opus)?; let stream = file.streams.get(s)?; Some((|| { let head = stream.headers.first().ok_or(MediaError::Corrupt("Opus header missing".into()))?; - let mut dec = slidecraft_opus::Decoder::new(head).map_err(|e| MediaError::Corrupt(e.to_string()))?; + let mut dec = deckcraft_opus::Decoder::new(head).map_err(|e| MediaError::Corrupt(e.to_string()))?; dec.set_trim_pre_skip(true); let channels = dec.channels() as u16; - let timing = slidecraft_ogg::OpusTiming::of(stream, dec.pre_skip() as u32); + let timing = deckcraft_ogg::OpusTiming::of(stream, dec.pre_skip() as u32); let mut out = Vec::new(); for i in 0..stream.packets.len() { let Ok(p) = file.read_packet(src, s, i) else { continue }; @@ -113,13 +113,13 @@ fn decode_ogg_opus(bytes: &Bytes) -> Option> { /// WebM / Matroska Opus with our decoder; `None` when there is no Opus track. fn decode_mkv_opus(bytes: &Bytes) -> Option> { let src: &[u8] = bytes; - let file = slidecraft_matroska::open(src).ok()?; + let file = deckcraft_matroska::open(src).ok()?; let (ti, head) = file.tracks.iter().enumerate().find_map(|(i, t)| match &t.codec { - slidecraft_matroska::Codec::Opus { head } => Some((i, head.clone())), + deckcraft_matroska::Codec::Opus { head } => Some((i, head.clone())), _ => None, })?; Some((|| { - let mut dec = slidecraft_opus::Decoder::new(&head).map_err(|e| MediaError::Corrupt(e.to_string()))?; + let mut dec = deckcraft_opus::Decoder::new(&head).map_err(|e| MediaError::Corrupt(e.to_string()))?; dec.set_trim_pre_skip(true); let channels = dec.channels() as u16; let mut out = Vec::new(); diff --git a/crates/media/src/feed.rs b/crates/media/src/feed.rs index b430995..721be85 100644 --- a/crates/media/src/feed.rs +++ b/crates/media/src/feed.rs @@ -42,7 +42,7 @@ impl VideoFeed { let (tx, cmd_rx) = std::sync::mpsc::channel::(); let (frame_tx, rx) = std::sync::mpsc::sync_channel::<(u64, Option)>(AHEAD); let worker = move || worker(dec, cmd_rx, frame_tx); - if let Err(e) = std::thread::Builder::new().name("slidecraft-video-decode".into()).spawn(worker) { + if let Err(e) = std::thread::Builder::new().name("deckcraft-video-decode".into()).spawn(worker) { return Err(crate::MediaError::Corrupt(format!("couldn't start the decoder: {e}"))); } Ok(VideoFeed { size, current: None, next: None, generation: 0, ended: false, tx, rx }) diff --git a/crates/media/src/lib.rs b/crates/media/src/lib.rs index 91a9241..d31b5d6 100644 --- a/crates/media/src/lib.rs +++ b/crates/media/src/lib.rs @@ -1,4 +1,4 @@ -//! SlideCraft media: everything between embedded media bytes and the speakers / the screen. +//! DeckCraft media: everything between embedded media bytes and the speakers / the screen. //! //! - [`probe`]: container, duration, audio channels / sample rate, video size / frame rate. //! - [`audio::decode`]: every common audio format to interleaved f32 [`audio::Pcm`] — MP3, AAC diff --git a/crates/media/src/player.rs b/crates/media/src/player.rs index 9a07e6f..9ac4a80 100644 --- a/crates/media/src/player.rs +++ b/crates/media/src/player.rs @@ -226,7 +226,7 @@ impl Player { let _ = s.set(crate::audio::decode(&b).map(Arc::new).map_err(|e| e.to_string())); }; if crate::THREADS { - if std::thread::Builder::new().name("slidecraft-audio-decode".into()).spawn(job).is_err() { + if std::thread::Builder::new().name("deckcraft-audio-decode".into()).spawn(job).is_err() { let _ = slot.set(Err("couldn't start the decoder".into())); } } else { diff --git a/crates/media/src/probe.rs b/crates/media/src/probe.rs index 2b8ad5c..80a8a38 100644 --- a/crates/media/src/probe.rs +++ b/crates/media/src/probe.rs @@ -9,7 +9,7 @@ pub struct AudioInfo { pub codec: String, pub sample_rate: u32, pub channels: u16, - /// SlideCraft can decode it. + /// DeckCraft can decode it. pub decodable: bool, } @@ -93,9 +93,9 @@ fn codec_label(short: &str) -> String { } fn probe_mp4(bytes: &Bytes) -> Result { - use slidecraft_isobmff::{CodecConfig, TrackKind}; + use deckcraft_isobmff::{CodecConfig, TrackKind}; let src: &[u8] = bytes; - let f = slidecraft_isobmff::open(src).map_err(|e| MediaError::Corrupt(e.to_string()))?; + let f = deckcraft_isobmff::open(src).map_err(|e| MediaError::Corrupt(e.to_string()))?; let mut info = MediaInfo { container: if f.is_quicktime { "mov" } else { "mp4" }, ..Default::default() }; let mut dur = if f.timescale > 0 { f.duration as f64 / f.timescale as f64 } else { 0.0 }; if let Some(ti) = f.track_of_kind(TrackKind::Video) { @@ -133,9 +133,9 @@ fn probe_mp4(bytes: &Bytes) -> Result { } fn probe_mkv(bytes: &Bytes) -> Result { - use slidecraft_matroska::{Codec, TrackKind}; + use deckcraft_matroska::{Codec, TrackKind}; let src: &[u8] = bytes; - let f = slidecraft_matroska::open(src).map_err(|e| MediaError::Corrupt(e.to_string()))?; + let f = deckcraft_matroska::open(src).map_err(|e| MediaError::Corrupt(e.to_string()))?; let mut info = MediaInfo { container: if f.is_webm() { "webm" } else { "matroska" }, ..Default::default() }; let dur = f.duration_ns().map(|n| n as f64 / 1e9).unwrap_or(0.0); info.duration_ms = ms(dur); @@ -166,11 +166,11 @@ fn probe_mkv(bytes: &Bytes) -> Result { fn probe_ogg(bytes: &Bytes) -> Option { let src: &[u8] = bytes; - let f = slidecraft_ogg::open(src).ok()?; - let s = f.stream_of(slidecraft_ogg::Codec::Opus)?; + let f = deckcraft_ogg::open(src).ok()?; + let s = f.stream_of(deckcraft_ogg::Codec::Opus)?; let st = f.streams.get(s)?; - let head = slidecraft_opus::OpusHead::parse(st.headers.first()?).ok()?; - let timing = slidecraft_ogg::OpusTiming::of(st, head.pre_skip as u32); + let head = deckcraft_opus::OpusHead::parse(st.headers.first()?).ok()?; + let timing = deckcraft_ogg::OpusTiming::of(st, head.pre_skip as u32); Some(MediaInfo { container: "ogg", duration_ms: ms(timing.total as f64 / 48_000.0), diff --git a/crates/media/src/video.rs b/crates/media/src/video.rs index 882f5b3..bd40c44 100644 --- a/crates/media/src/video.rs +++ b/crates/media/src/video.rs @@ -3,8 +3,8 @@ use std::collections::VecDeque; -use slidecraft_isobmff::CodecConfig; -use slidecraft_matroska::Codec as MkvCodec; +use deckcraft_isobmff::CodecConfig; +use deckcraft_matroska::Codec as MkvCodec; use crate::yuv::{self, Matrix, Planes}; use crate::{Bytes, Container, MediaError, Result}; @@ -41,15 +41,15 @@ fn dec_err(e: impl std::fmt::Display) -> MediaError { struct H264 { avcc: Vec, - dec: slidecraft_h264::Decoder, + dec: deckcraft_h264::Decoder, } impl H264 { fn new(avcc: Vec) -> Result { - let dec = slidecraft_h264::Decoder::from_avcc(&avcc).map_err(dec_err)?; + let dec = deckcraft_h264::Decoder::from_avcc(&avcc).map_err(dec_err)?; Ok(H264 { avcc, dec }) } - fn convert(p: slidecraft_h264::Picture) -> Pic { + fn convert(p: deckcraft_h264::Picture) -> Pic { let (w, h) = (p.width as usize, p.height as usize); let planes = Planes { y: &p.y, @@ -77,7 +77,7 @@ impl Codec for H264 { self.dec.flush().into_iter().map(H264::convert).collect() } fn reset(&mut self) { - if let Ok(d) = slidecraft_h264::Decoder::from_avcc(&self.avcc) { + if let Ok(d) = deckcraft_h264::Decoder::from_avcc(&self.avcc) { self.dec = d; } } @@ -120,19 +120,19 @@ macro_rules! plane_pic { struct Hevc { hvcc: Vec, - dec: slidecraft_hevc::Decoder, + dec: deckcraft_hevc::Decoder, } impl Hevc { fn new(hvcc: Vec) -> Result { - let dec = slidecraft_hevc::Decoder::from_hvcc(&hvcc).map_err(dec_err)?; + let dec = deckcraft_hevc::Decoder::from_hvcc(&hvcc).map_err(dec_err)?; Ok(Hevc { hvcc, dec }) } - fn convert(p: slidecraft_hevc::Picture) -> Pic { + fn convert(p: deckcraft_hevc::Picture) -> Pic { let sx = u32::from(p.chroma_width < p.width); let sy = u32::from(p.chroma_height < p.height); let m = Matrix::from_code(p.color.matrix, p.height); - plane_pic!(&p, slidecraft_hevc::Plane, sx, sy, m, p.color.full_range, p.chroma_width == 0) + plane_pic!(&p, deckcraft_hevc::Plane, sx, sy, m, p.color.full_range, p.chroma_width == 0) } } @@ -144,23 +144,23 @@ impl Codec for Hevc { self.dec.flush().into_iter().map(Hevc::convert).collect() } fn reset(&mut self) { - if let Ok(d) = slidecraft_hevc::Decoder::from_hvcc(&self.hvcc) { + if let Ok(d) = deckcraft_hevc::Decoder::from_hvcc(&self.hvcc) { self.dec = d; } } } struct Vp9 { - dec: slidecraft_vp9::Decoder, + dec: deckcraft_vp9::Decoder, } impl Vp9 { - fn convert(p: slidecraft_vp9::Picture) -> Pic { + fn convert(p: deckcraft_vp9::Picture) -> Pic { let (w, h) = (p.width as usize, p.height as usize); if p.color.color_space == 7 { // RGB (4:4:4): the planes carry G, B, R. let shift = p.bit_depth.saturating_sub(8); - let at = |pl: &slidecraft_vp9::Plane, i: usize| if i < pl.len() { (pl.get(i) >> shift) as u8 } else { 0 }; + let at = |pl: &deckcraft_vp9::Plane, i: usize| if i < pl.len() { (pl.get(i) >> shift) as u8 } else { 0 }; let mut rgba = Vec::with_capacity(w * h * 4); for row in 0..h { for col in 0..w { @@ -178,7 +178,7 @@ impl Vp9 { 5 => Matrix::Bt2020, _ => Matrix::from_code(2, p.height), }; - plane_pic!(&p, slidecraft_vp9::Plane, u32::from(p.subsampling_x), u32::from(p.subsampling_y), m, p.color.full_range, false) + plane_pic!(&p, deckcraft_vp9::Plane, u32::from(p.subsampling_x), u32::from(p.subsampling_y), m, p.color.full_range, false) } } @@ -195,13 +195,13 @@ impl Codec for Vp9 { } struct Av1 { - dec: slidecraft_av1::Decoder, + dec: deckcraft_av1::Decoder, config_obus: Vec, primed: bool, } impl Av1 { - fn convert(p: slidecraft_av1::Picture) -> Pic { + fn convert(p: deckcraft_av1::Picture) -> Pic { let (w, h) = (p.width as usize, p.height as usize); let cw = p.plane_width(1) as usize; let rgba = yuv::to_rgba( @@ -239,7 +239,7 @@ impl Codec for Av1 { self.dec.flush().into_iter().map(Av1::convert).collect() } fn reset(&mut self) { - self.dec = slidecraft_av1::Decoder::new(); + self.dec = deckcraft_av1::Decoder::new(); self.primed = false; } } @@ -257,8 +257,8 @@ struct Track { } enum Demux { - Mp4(Box, usize), - Mkv(Box, usize), + Mp4(Box, usize), + Mkv(Box, usize), } impl Demux { @@ -273,15 +273,15 @@ impl Demux { fn open_track(bytes: &[u8]) -> Result<(Demux, Track, Box)> { match Container::sniff(bytes) { Container::Mp4 => { - let f = slidecraft_isobmff::open(bytes).map_err(dec_err)?; - let ti = f.track_of_kind(slidecraft_isobmff::TrackKind::Video).ok_or(MediaError::Missing("video track"))?; + let f = deckcraft_isobmff::open(bytes).map_err(dec_err)?; + let ti = f.track_of_kind(deckcraft_isobmff::TrackKind::Video).ok_or(MediaError::Missing("video track"))?; let t = &f.tracks[ti]; let entry = t.entries.first().ok_or(MediaError::Missing("video track"))?; let codec: Box = match &entry.codec { CodecConfig::Avc(a) => Box::new(H264::new(a.to_bytes())?), CodecConfig::Hevc(c) => Box::new(Hevc::new(c.to_bytes())?), - CodecConfig::Vp9(_) => Box::new(Vp9 { dec: slidecraft_vp9::Decoder::new() }), - CodecConfig::Av1(c) => Box::new(Av1 { dec: slidecraft_av1::Decoder::new(), config_obus: c.config_obus.clone(), primed: false }), + CodecConfig::Vp9(_) => Box::new(Vp9 { dec: deckcraft_vp9::Decoder::new() }), + CodecConfig::Av1(c) => Box::new(Av1 { dec: deckcraft_av1::Decoder::new(), config_obus: c.config_obus.clone(), primed: false }), other => return Err(MediaError::Unsupported(format!("{} video", codec_name(other)))), }; let (w, h) = entry.video.as_ref().map(|v| (v.width as u32, v.height as u32)).unwrap_or((t.width, t.height)); @@ -290,20 +290,20 @@ fn open_track(bytes: &[u8]) -> Result<(Demux, Track, Box)> { Ok((Demux::Mp4(Box::new(f), ti), track, codec)) } Container::Matroska => { - let f = slidecraft_matroska::open(bytes).map_err(dec_err)?; + let f = deckcraft_matroska::open(bytes).map_err(dec_err)?; let ti = f .tracks .iter() - .position(|t| t.kind == slidecraft_matroska::TrackKind::Video && !t.samples.is_empty()) + .position(|t| t.kind == deckcraft_matroska::TrackKind::Video && !t.samples.is_empty()) .ok_or(MediaError::Missing("video track"))?; let t = &f.tracks[ti]; let codec: Box = match &t.codec { MkvCodec::Avc { avcc } => Box::new(H264::new(avcc.clone())?), MkvCodec::Hevc { hvcc } => Box::new(Hevc::new(hvcc.clone())?), - MkvCodec::Vp9 { .. } => Box::new(Vp9 { dec: slidecraft_vp9::Decoder::new() }), + MkvCodec::Vp9 { .. } => Box::new(Vp9 { dec: deckcraft_vp9::Decoder::new() }), MkvCodec::Av1 { av1c } => Box::new(Av1 { - dec: slidecraft_av1::Decoder::new(), - config_obus: slidecraft_isobmff::Av1Config::parse(av1c).map(|c| c.config_obus).unwrap_or_default(), + dec: deckcraft_av1::Decoder::new(), + config_obus: deckcraft_isobmff::Av1Config::parse(av1c).map(|c| c.config_obus).unwrap_or_default(), primed: false, }), other => return Err(MediaError::Unsupported(format!("{} video", mkv_codec_name(other)))), diff --git a/crates/media/tests/decode.rs b/crates/media/tests/decode.rs index e3a6be7..b317944 100644 --- a/crates/media/tests/decode.rs +++ b/crates/media/tests/decode.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use std::time::Duration; -use slidecraft_media::{Bytes, Container, MediaError, audio, probe, video}; +use deckcraft_media::{Bytes, Container, MediaError, audio, probe, video}; fn fixture(name: &str) -> Bytes { let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(name); @@ -299,7 +299,7 @@ fn av1_mkv() { #[test] fn video_feed_follows_the_clock() { - let mut feed = slidecraft_media::VideoFeed::new(fixture("clip-h264.mov")).expect("feed"); + let mut feed = deckcraft_media::VideoFeed::new(fixture("clip-h264.mov")).expect("feed"); assert_eq!(feed.size(), (64, 48)); let t = Duration::from_secs(5); assert!(feed.frame_at_blocking(0.0, t).is_some_and(|f| f.time == 0.0)); @@ -314,7 +314,7 @@ fn video_feed_follows_the_clock() { #[test] fn player_plays_decoded_audio_through_the_mixer() { - use slidecraft_media::{ClipParams, Player, VoiceSpec}; + use deckcraft_media::{ClipParams, Player, VoiceSpec}; let b: Bytes = Arc::new(wav(48_000, 1, &sine(48_000, 0.5))); let mut p = Player::new(None); p.play(1, 1, &b, VoiceSpec { clip: ClipParams { volume: 0.5, ..Default::default() }, looping: false, duration: 0.5, audio: true }, None); diff --git a/crates/media/tests/fixtures/make.sh b/crates/media/tests/fixtures/make.sh index 463cffb..bf09b67 100755 --- a/crates/media/tests/fixtures/make.sh +++ b/crates/media/tests/fixtures/make.sh @@ -27,11 +27,11 @@ for f in range(6): sys.stdout.buffer.write(bytes(v for y in range(h) for x in range(w) for v in ((x * 4 + f * 20) % 256, (y * 5) % 256, (f * 40) % 256))) " > "$3" } -frames 64 48 /tmp/slidecraft-fixture-64x48.rgb -frames 64 64 /tmp/slidecraft-fixture-64x64.rgb -VID="-f rawvideo -pix_fmt rgb24 -s 64x48 -r 10 -i /tmp/slidecraft-fixture-64x48.rgb" +frames 64 48 /tmp/deckcraft-fixture-64x48.rgb +frames 64 64 /tmp/deckcraft-fixture-64x64.rgb +VID="-f rawvideo -pix_fmt rgb24 -s 64x48 -r 10 -i /tmp/deckcraft-fixture-64x48.rgb" $FF $VID $TONE44 -c:v libx264 -pix_fmt yuv420p -g 5 -bf 2 -c:a aac -b:a 48k -shortest $META clip-h264.mp4 $FF $VID -c:v libx264 -pix_fmt yuv420p -g 5 -bf 0 $META clip-h264.mov $FF $VID -c:v libx265 -pix_fmt yuv420p -x265-params log-level=error:keyint=5 -tag:v hvc1 $META clip-hevc.mp4 $FF $VID $TONE -c:v libvpx-vp9 -pix_fmt yuv420p -g 5 -b:v 100k -c:a libopus -b:a 32k -shortest $META clip-vp9.webm -$FF -f rawvideo -pix_fmt rgb24 -s 64x64 -r 10 -i /tmp/slidecraft-fixture-64x64.rgb -c:v libsvtav1 -pix_fmt yuv420p -g 5 -svtav1-params loglevel=0 $META clip-av1.mkv +$FF -f rawvideo -pix_fmt rgb24 -s 64x64 -r 10 -i /tmp/deckcraft-fixture-64x64.rgb -c:v libsvtav1 -pix_fmt yuv420p -g 5 -svtav1-params loglevel=0 $META clip-av1.mkv diff --git a/crates/model/Cargo.toml b/crates/model/Cargo.toml index c0db212..cb6b4f1 100644 --- a/crates/model/Cargo.toml +++ b/crates/model/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "slidecraft-model" +name = "deckcraft-model" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft document model: presentations, masters, layouts, slides, shapes, text, themes, transitions, animations" +description = "DeckCraft document model: presentations, masters, layouts, slides, shapes, text, themes, transitions, animations" [lints] workspace = true [dependencies] -slidecraft-geom = { workspace = true } -slidecraft-color = { workspace = true } +deckcraft-geom = { workspace = true } +deckcraft-color = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/model/src/defaults.rs b/crates/model/src/defaults.rs index 09e0e3a..8a2b5b4 100644 --- a/crates/model/src/defaults.rs +++ b/crates/model/src/defaults.rs @@ -3,8 +3,8 @@ use std::sync::Arc; -use slidecraft_color::SchemeSlot; -use slidecraft_geom::{Size, Xfrm}; +use deckcraft_color::SchemeSlot; +use deckcraft_geom::{Size, Xfrm}; use crate::style::ColorRef; use crate::text::{Anchor, AutoFit, BodyProps, Bullet, LevelStyle, ListStyle, ParaProps, RunProps, Spacing, TextBody, TextDir}; @@ -175,7 +175,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) { para: ParaProps { align: Some(align), ..Default::default() }, run: RunProps { size: Some(12.0), - fill: Some(Fill::solid(ColorRef::scheme(SchemeSlot::Tx1).with(slidecraft_color::ColorTransform::Tint(75000)))), + fill: Some(Fill::solid(ColorRef::scheme(SchemeSlot::Tx1).with(deckcraft_color::ColorTransform::Tint(75000)))), ..Default::default() }, }, @@ -320,7 +320,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) { para: ParaProps { margin_left: Some(0.0), indent: Some(0.0), bullet: Some(Bullet::None), ..Default::default() }, run: RunProps { size: Some(24.0), - fill: Some(Fill::solid(ColorRef::scheme(SchemeSlot::Tx1).with(slidecraft_color::ColorTransform::Tint(82000)))), + fill: Some(Fill::solid(ColorRef::scheme(SchemeSlot::Tx1).with(deckcraft_color::ColorTransform::Tint(82000)))), ..Default::default() }, }, diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index a7845cc..6940fa4 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -1,4 +1,4 @@ -//! The SlideCraft document model. +//! The DeckCraft document model. //! //! A [`Presentation`] has slide masters (each with a theme and layouts) and slides. Each slide //! uses a layout; placeholders on a slide inherit position, formatting and text styles from the @@ -19,9 +19,9 @@ pub mod theme; use std::sync::Arc; +pub use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot}; +pub use deckcraft_geom::{Rect, Size, Xfrm}; use serde::{Deserialize, Serialize}; -pub use slidecraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot}; -pub use slidecraft_geom::{Rect, Size, Xfrm}; pub use anim::{AnimClass, AnimStart, Animation, Transition}; pub use chart::Chart; @@ -208,13 +208,13 @@ pub struct CustomPath { pub h: f64, /// `M x y`, `L x y`, `C x1 y1 x2 y2 x y`, `Q x1 y1 x y`, `Z`. pub d: String, - pub fill: slidecraft_geom::preset::FillMode, + pub fill: deckcraft_geom::preset::FillMode, pub stroke: bool, } impl Default for CustomPath { fn default() -> Self { - CustomPath { w: 0.0, h: 0.0, d: String::new(), fill: slidecraft_geom::preset::FillMode::Norm, stroke: true } + CustomPath { w: 0.0, h: 0.0, d: String::new(), fill: deckcraft_geom::preset::FillMode::Norm, stroke: true } } } @@ -371,7 +371,7 @@ impl Shape { } /// Is this a line-like shape (no area)? pub fn is_line(&self) -> bool { - matches!(self.kind, ShapeKind::Connector { .. }) || self.geom.preset_name().is_some_and(slidecraft_geom::preset::is_line_like) + matches!(self.kind, ShapeKind::Connector { .. }) || self.geom.preset_name().is_some_and(deckcraft_geom::preset::is_line_like) } pub fn kind_name(&self) -> &'static str { match &self.kind { diff --git a/crates/model/src/resolve.rs b/crates/model/src/resolve.rs index 2abab4b..44e7a8d 100644 --- a/crates/model/src/resolve.rs +++ b/crates/model/src/resolve.rs @@ -1,8 +1,8 @@ //! Inheritance: effective position, fill, line, body and text properties of a shape, following //! slide → layout placeholder → master placeholder → master text styles → theme. -use slidecraft_color::{ColorScheme, Rgba, SchemeSlot}; -use slidecraft_geom::Xfrm; +use deckcraft_color::{ColorScheme, Rgba, SchemeSlot}; +use deckcraft_geom::Xfrm; use crate::style::{ColorRef, Effects, Fill, Line}; use crate::text::{Anchor, AutoFit, BodyProps, LevelStyle, ListStyle, ParaProps, RunProps}; diff --git a/crates/model/src/style.rs b/crates/model/src/style.rs index fc51251..ff1b4e0 100644 --- a/crates/model/src/style.rs +++ b/crates/model/src/style.rs @@ -1,7 +1,7 @@ //! Colours, fills, lines and effects. +use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot}; use serde::{Deserialize, Serialize}; -use slidecraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot}; use crate::MediaId; @@ -50,10 +50,10 @@ impl ColorRef { ColorBase::Rgb { rgb } => *rgb, ColorBase::Scheme { slot: SchemeSlot::PhClr } => ph.unwrap_or_else(|| scheme.get(SchemeSlot::Accent1)), ColorBase::Scheme { slot } => scheme.get(*slot), - ColorBase::Preset { name } => slidecraft_color::preset(name).unwrap_or(Rgba::BLACK), + ColorBase::Preset { name } => deckcraft_color::preset(name).unwrap_or(Rgba::BLACK), ColorBase::System { last, .. } => *last, }; - slidecraft_color::apply(base, &self.mods) + deckcraft_color::apply(base, &self.mods) } /// Alpha (0–1) from an `alpha` transform, if any. pub fn alpha(&self) -> f64 { diff --git a/crates/model/src/tests.rs b/crates/model/src/tests.rs index f434be1..3c5db2e 100644 --- a/crates/model/src/tests.rs +++ b/crates/model/src/tests.rs @@ -11,7 +11,7 @@ fn new_presentation_is_valid() { let s = &p.slides[0]; let (m, l) = p.master_for(s).unwrap(); assert_eq!(l.unwrap().kind, LayoutType::Title); - assert_eq!(m.theme.name, "SlideCraft"); + assert_eq!(m.theme.name, "DeckCraft"); // Title slide has a centred title and a subtitle. let kinds: Vec<_> = s.shapes.iter().filter_map(|x| x.ph_type()).collect(); assert_eq!(kinds, vec![PhType::CtrTitle, PhType::SubTitle]); diff --git a/crates/model/src/theme.rs b/crates/model/src/theme.rs index 3a6ec4f..26912ea 100644 --- a/crates/model/src/theme.rs +++ b/crates/model/src/theme.rs @@ -1,7 +1,7 @@ //! Themes: colour scheme, font scheme and format scheme (the fill/line/effect "style matrix"). +use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot}; use serde::{Deserialize, Serialize}; -use slidecraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot}; use crate::style::{ColorRef, Effects, Fill, Gradient, GradientShape, GradientStop, Line, Shadow}; @@ -50,8 +50,8 @@ pub struct Theme { impl Default for Theme { fn default() -> Self { builtin_themes().into_iter().next().unwrap_or_else(|| Theme { - name: "SlideCraft".into(), - colors: ColorScheme { name: "SlideCraft".into(), colors: [Rgba::BLACK; 12] }, + name: "DeckCraft".into(), + colors: ColorScheme { name: "DeckCraft".into(), colors: [Rgba::BLACK; 12] }, fonts: FontScheme::default(), format: FormatScheme::default(), raw_extra: None, @@ -99,7 +99,7 @@ pub fn default_format() -> FormatScheme { }) }; FormatScheme { - name: "SlideCraft".into(), + name: "DeckCraft".into(), fills: vec![Fill::solid(ph()), grad(110000, 92000), grad(105000, 80000)], lines: vec![Line::solid(ph(), 0.75), Line::solid(ph(), 1.0), Line::solid(ph(), 1.5)], effects: vec![ @@ -145,9 +145,9 @@ pub fn builtin_themes() -> Vec { }; vec![ t( - "SlideCraft", + "DeckCraft", scheme( - "SlideCraft", + "DeckCraft", ["000000", "FFFFFF", "1E2B3C", "EDEBE7", "2E6FD8", "F26B1D", "2EA36F", "8A5CD8", "D83F6B", "E3B21C", "2E6FD8", "7A5BA6"], ), "Inter", diff --git a/crates/ogg/Cargo.toml b/crates/ogg/Cargo.toml index f30ddf4..9e38883 100644 --- a/crates/ogg/Cargo.toml +++ b/crates/ogg/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-ogg" +name = "deckcraft-ogg" description = "Clean-room Ogg (RFC 3533) demuxer with Ogg Opus (RFC 7845) and Ogg Vorbis stream mapping" version.workspace = true edition.workspace = true diff --git a/crates/opus/Cargo.toml b/crates/opus/Cargo.toml index e911357..257cd81 100644 --- a/crates/opus/Cargo.toml +++ b/crates/opus/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-opus" +name = "deckcraft-opus" description = "Clean-room pure-Rust Opus decoder (RFC 6716 / RFC 8251; OpusHead, multistream surround)" version.workspace = true edition.workspace = true diff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml index 3e4e367..50f9adb 100644 --- a/crates/pdf/Cargo.toml +++ b/crates/pdf/Cargo.toml @@ -1,18 +1,18 @@ [package] -name = "slidecraft-pdf" +name = "deckcraft-pdf" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft PDF export (krilla): slides, notes pages and handouts with a selectable text layer, links and bookmarks" +description = "DeckCraft PDF export (krilla): slides, notes pages and handouts with a selectable text layer, links and bookmarks" [lints] workspace = true [dependencies] -slidecraft-model = { workspace = true } -slidecraft-fonts = { workspace = true } -slidecraft-text = { workspace = true } -slidecraft-render = { workspace = true } +deckcraft-model = { workspace = true } +deckcraft-fonts = { workspace = true } +deckcraft-text = { workspace = true } +deckcraft-render = { workspace = true } krilla = { workspace = true } kurbo = { workspace = true } serde = { workspace = true } diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index f48670a..97eb641 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -1,6 +1,6 @@ //! PDF export. //! -//! Each slide is drawn by the SlideCraft renderer at print resolution (so the PDF looks exactly like +//! Each slide is drawn by the DeckCraft renderer at print resolution (so the PDF looks exactly like //! the slide, effects included) and overlaid with an invisible layer of real text from the text //! layout engine with embedded font subsets, so the text can be searched, selected and copied. //! Hyperlinks and shape actions become link annotations, slide titles become bookmarks. @@ -10,6 +10,10 @@ use std::collections::HashMap; +use deckcraft_fonts::FontDb; +use deckcraft_model::Presentation; +use deckcraft_model::text::Action as ClickAction; +use deckcraft_render::{PlacedText, RenderOpts}; use krilla::action::{Action, LinkAction}; use krilla::annotation::{Annotation, LinkAnnotation, Target}; use krilla::destination::XyzDestination; @@ -24,10 +28,6 @@ use krilla::surface::Surface; use krilla::text::{Font, GlyphId, KrillaGlyph, TextDirection}; use kurbo::{Affine, Rect}; use serde::{Deserialize, Serialize}; -use slidecraft_fonts::FontDb; -use slidecraft_model::Presentation; -use slidecraft_model::text::Action as ClickAction; -use slidecraft_render::{PlacedText, RenderOpts}; #[derive(Debug, thiserror::Error)] pub enum PdfError { @@ -97,7 +97,7 @@ pub fn export(pres: &Presentation, opts: &PdfOptions) -> Result> { } let mut pdf = krilla::Document::new(); let title = opts.title.clone().unwrap_or_else(|| pres.props.title.clone()); - let mut meta = Metadata::new().creator("SlideCraft".into()).producer("SlideCraft".into()); + let mut meta = Metadata::new().creator("DeckCraft".into()).producer("DeckCraft".into()); if !title.trim().is_empty() { meta = meta.title(title); } @@ -228,7 +228,7 @@ fn grey(v: u8, opacity: f32) -> Fill { } impl Exporter<'_> { - fn font(&mut self, face: &slidecraft_fonts::FontFace) -> Option { + fn font(&mut self, face: &deckcraft_fonts::FontFace) -> Option { self.fonts.entry(face.id()).or_insert_with(|| Font::new(face.data().to_vec().into(), face.index())).clone() } @@ -239,7 +239,7 @@ impl Exporter<'_> { let k = b.width() / sw; // Raster at the requested dpi relative to the box size on paper. let scale = (b.width() / 72.0 * self.opts.dpi.clamp(36.0, 600.0)) / sw; - let img = slidecraft_render::render_slide(pres, i, &RenderOpts { scale, threads: 4, ..Default::default() }); + let img = deckcraft_render::render_slide(pres, i, &RenderOpts { scale, threads: 4, ..Default::default() }); if img.width > 0 && img.height > 0 { let mut rgba = img.pixels.clone(); for px in rgba.as_chunks_mut::<4>().0 { @@ -272,7 +272,7 @@ impl Exporter<'_> { } if self.opts.text_layer { let view = Affine::translate((b.x0, b.y0)) * Affine::scale(k); - for pt in slidecraft_render::place_slide(pres, i).texts { + for pt in deckcraft_render::place_slide(pres, i).texts { self.text_layer(s, &pt, view); } let _ = sh; @@ -332,7 +332,7 @@ impl Exporter<'_> { let k = b.width() / pres.slide_size.width.max(1.0); let view = Affine::translate((b.x0, b.y0)) * Affine::scale(k); let mut out = Vec::new(); - for l in slidecraft_render::place_slide(pres, i).links { + for l in deckcraft_render::place_slide(pres, i).links { let r = view.transform_rect_bbox(l.rect).intersect(b); let Some(rect) = krilla::geom::Rect::from_ltrb(r.x0 as f32, r.y0 as f32, r.x1 as f32, r.y1 as f32) else { continue }; let goto = |slide: Option| { @@ -355,8 +355,8 @@ impl Exporter<'_> { out } - fn ui_font(&mut self) -> Option<(Font, std::sync::Arc)> { - let face = FontDb::global().face(slidecraft_fonts::DEFAULT_FAMILY, "Regular"); + fn ui_font(&mut self) -> Option<(Font, std::sync::Arc)> { + let face = FontDb::global().face(deckcraft_fonts::DEFAULT_FAMILY, "Regular"); let f = self.font(&face)?; Some((f, face)) } diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index c6d8a72..305f145 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -1,13 +1,13 @@ use std::sync::Arc; use super::*; -use slidecraft_model::text::TextBody; +use deckcraft_model::text::TextBody; fn deck(n: usize) -> Presentation { - let mut p = slidecraft_model::defaults::new_presentation(None); + let mut p = deckcraft_model::defaults::new_presentation(None); let layout = p.slides[0].layout; while p.slides.len() < n { - let s = slidecraft_model::defaults::new_slide(&mut p, layout); + let s = deckcraft_model::defaults::new_slide(&mut p, layout); p.slides.push(Arc::new(s)); } for (i, s) in p.slides.iter_mut().enumerate() { diff --git a/crates/pptx/Cargo.toml b/crates/pptx/Cargo.toml index 5074e90..0ea79d6 100644 --- a/crates/pptx/Cargo.toml +++ b/crates/pptx/Cargo.toml @@ -1,26 +1,26 @@ [package] -name = "slidecraft-pptx" +name = "deckcraft-pptx" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft PPTX: Office Open XML (PresentationML/DrawingML) import and export, written from the public ECMA-376 standard" +description = "DeckCraft PPTX: Office Open XML (PresentationML/DrawingML) import and export, written from the public ECMA-376 standard" [lints] workspace = true [dependencies] -slidecraft-model = { workspace = true } -slidecraft-geom = { workspace = true } -slidecraft-color = { workspace = true } +deckcraft-model = { workspace = true } +deckcraft-geom = { workspace = true } +deckcraft-color = { workspace = true } quick-xml = { workspace = true } zip = { workspace = true } thiserror = { workspace = true } log = { workspace = true } [dev-dependencies] -slidecraft-engine = { workspace = true } +deckcraft-engine = { workspace = true } serde_json = { workspace = true } -slidecraft-color = { workspace = true } -slidecraft-geom = { workspace = true } +deckcraft-color = { workspace = true } +deckcraft-geom = { workspace = true } image = { workspace = true } zip = { workspace = true } diff --git a/crates/pptx/src/custgeom.rs b/crates/pptx/src/custgeom.rs index 34b649b..ae12214 100644 --- a/crates/pptx/src/custgeom.rs +++ b/crates/pptx/src/custgeom.rs @@ -8,8 +8,8 @@ use std::collections::HashMap; use std::f64::consts::PI; use std::fmt::Write as _; -use slidecraft_geom::preset::FillMode; -use slidecraft_model::CustomPath; +use deckcraft_geom::preset::FillMode; +use deckcraft_model::CustomPath; use crate::xml::{A, El, W}; diff --git a/crates/pptx/src/lib.rs b/crates/pptx/src/lib.rs index a25e20a..a6018b7 100644 --- a/crates/pptx/src/lib.rs +++ b/crates/pptx/src/lib.rs @@ -2,7 +2,7 @@ //! ECMA-376 standard. //! //! [`import`] reads a package leniently (unknown parts are skipped, malformed parts are logged and -//! ignored, every size and depth is bounded) into the SlideCraft model; [`export`] writes a valid +//! ignored, every size and depth is bounded) into the DeckCraft model; [`export`] writes a valid //! package that PowerPoint opens without repair. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] @@ -16,7 +16,7 @@ mod tables; mod write; mod xml; -use slidecraft_model::Presentation; +use deckcraft_model::Presentation; /// Extension URI of the PowerPoint 2010 section list in `presentation.xml`. pub(crate) const SECTION_EXT_URI: &str = "{521415D9-36F7-43E2-AB2F-B90AF26B5E84}"; diff --git a/crates/pptx/src/maps.rs b/crates/pptx/src/maps.rs index 520f958..c700d8e 100644 --- a/crates/pptx/src/maps.rs +++ b/crates/pptx/src/maps.rs @@ -1,7 +1,7 @@ //! Mappings between our transition / animation ids and PresentationML elements and preset //! numbers, shared by the reader and the writer. -use slidecraft_model::anim::{ANIMATIONS, AnimClass}; +use deckcraft_model::anim::{ANIMATIONS, AnimClass}; use crate::xml::{A, El, W}; @@ -13,7 +13,7 @@ const SIMPLE: &[(&str, &str, &str)] = pub fn transition_from_xml(e: &El) -> Option<(String, String)> { let (k, o) = transition_from_xml_raw(e)?; // Kinds without options keep none. - let has_opts = slidecraft_model::anim::TRANSITIONS.iter().find(|t| t.0 == k).is_none_or(|t| !t.4.is_empty()); + let has_opts = deckcraft_model::anim::TRANSITIONS.iter().find(|t| t.0 == k).is_none_or(|t| !t.4.is_empty()); Some((k, if has_opts { o } else { String::new() })) } @@ -78,7 +78,7 @@ fn transition_from_xml_raw(e: &El) -> Option<(String, String)> { } "prstTrans" => { let prst = e.attr("prst").unwrap_or("fallOver"); - let known = slidecraft_model::anim::TRANSITIONS.iter().any(|t| t.0 == prst); + let known = deckcraft_model::anim::TRANSITIONS.iter().any(|t| t.0 == prst); if !known { return None; } @@ -303,7 +303,7 @@ pub fn fallback_effect(class: AnimClass) -> &'static str { } /// The preset id to write for an animation. -pub fn preset_for(a: &slidecraft_model::Animation) -> u32 { +pub fn preset_for(a: &deckcraft_model::Animation) -> u32 { if let Some(p) = a.preset_id { let consistent = match effect_for_preset(a.class, p) { Some(e) => e == a.effect, @@ -313,7 +313,7 @@ pub fn preset_for(a: &slidecraft_model::Animation) -> u32 { return p; } } - match slidecraft_model::anim::animation_info(&a.effect, a.class) { + match deckcraft_model::anim::animation_info(&a.effect, a.class) { Some(info) if info.2 == a.class => info.3, _ => match a.class { AnimClass::Entrance | AnimClass::Exit => 10, @@ -331,7 +331,7 @@ mod tests { #[test] fn every_transition_kind_round_trips() { - for (kind, _, _, _, opts) in slidecraft_model::anim::TRANSITIONS { + for (kind, _, _, _, opts) in deckcraft_model::anim::TRANSITIONS { if *kind == "none" { continue; } diff --git a/crates/pptx/src/read/chart.rs b/crates/pptx/src/read/chart.rs index 52f0bd0..a9a960a 100644 --- a/crates/pptx/src/read/chart.rs +++ b/crates/pptx/src/read/chart.rs @@ -1,7 +1,7 @@ //! Chart parts (`c:chartSpace`) → [`Chart`]. The original XML is kept in `Chart.raw`. -use slidecraft_model::chart::{Chart, ChartType, Series}; -use slidecraft_model::style::Fill; +use deckcraft_model::chart::{Chart, ChartType, Series}; +use deckcraft_model::style::Fill; use crate::xml::El; @@ -19,9 +19,9 @@ fn simple_fill(sp: Option<&El>) -> Option { None } -fn simple_line(sp: Option<&El>) -> Option { +fn simple_line(sp: Option<&El>) -> Option { let ln = sp?.child("ln")?; - let mut l = slidecraft_model::Line { width: super::dml::pt(ln, "w"), ..Default::default() }; + let mut l = deckcraft_model::Line { width: super::dml::pt(ln, "w"), ..Default::default() }; l.fill = simple_fill(Some(ln)); Some(l) } diff --git a/crates/pptx/src/read/dml.rs b/crates/pptx/src/read/dml.rs index 5ed63f4..b4f08e0 100644 --- a/crates/pptx/src/read/dml.rs +++ b/crates/pptx/src/read/dml.rs @@ -1,12 +1,12 @@ //! DrawingML reading: colours, fills, lines, effects, transforms and text. -use slidecraft_color::{ColorTransform, Rgba, SchemeSlot}; -use slidecraft_geom::{Xfrm, emu_to_pt}; -use slidecraft_model::style::{ +use deckcraft_color::{ColorTransform, Rgba, SchemeSlot}; +use deckcraft_geom::{Xfrm, emu_to_pt}; +use deckcraft_model::style::{ ColorBase, ColorRef, Compound, Dash, Effects, Fill, Glow, Gradient, GradientShape, GradientStop, Line, LineCap, LineEnd, LineJoin, PatternFill, PictureFill, PictureMode, Reflection, Shadow, }; -use slidecraft_model::text::{ +use deckcraft_model::text::{ Action, Align, Anchor, AutoFit, BodyProps, Bullet, Caps, Hyperlink, LevelStyle, ListStyle, ParaProps, Paragraph, Run, RunKind, RunProps, Spacing, Strike, TabStop, TextBody, TextDir, }; @@ -165,7 +165,7 @@ pub fn blip_fill(imp: &mut Imp, part: &Part, bf: &El) -> Option { } /// Media for an element with `r:embed` (or `r:link`). -pub fn media_ref(imp: &mut Imp, part: &Part, b: &El) -> Option { +pub fn media_ref(imp: &mut Imp, part: &Part, b: &El) -> Option { if let Some(id) = b.attr("r:embed").filter(|s| !s.is_empty()) && let Some(r) = part.rels.get(id) { diff --git a/crates/pptx/src/read/mod.rs b/crates/pptx/src/read/mod.rs index 57e1246..b8c403b 100644 --- a/crates/pptx/src/read/mod.rs +++ b/crates/pptx/src/read/mod.rs @@ -11,9 +11,9 @@ pub(crate) use chart::{chart_from as chart_from_el, read_chart_xml}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use slidecraft_geom::{Size, emu_to_pt}; -use slidecraft_model::text::TextBody; -use slidecraft_model::{ +use deckcraft_geom::{Size, emu_to_pt}; +use deckcraft_model::text::TextBody; +use deckcraft_model::{ Background, Comment, CustomShow, HeaderFooter, Layout, LayoutId, LayoutType, Master, MasterId, MediaId, MediaItem, PhType, Presentation, Properties, Section, ShowSettings, Slide, SlideId, defaults, }; @@ -422,12 +422,12 @@ pub fn color_map(cm: &El) -> Vec<(String, String)> { fn background(imp: &mut Imp, part: &Part, csld: &El) -> Option { let bg = csld.child("bg")?; if let Some(pr) = bg.child("bgPr") { - return Some(Background::Fill { fill: dml::fill_of(imp, part, pr).unwrap_or(slidecraft_model::Fill::None) }); + return Some(Background::Fill { fill: dml::fill_of(imp, part, pr).unwrap_or(deckcraft_model::Fill::None) }); } let r = bg.child("bgRef")?; Some(Background::Ref { idx: r.u32("idx").unwrap_or(1001), - color: dml::color(r).unwrap_or(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Bg1)), + color: dml::color(r).unwrap_or(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1)), }) } @@ -641,7 +641,7 @@ fn read_show(sp: &El, shows: &HashMap) -> ShowSettings { s.custom_show = Some(shows.get(id).cloned().unwrap_or_else(|| id.to_string())); } if let Some(c) = sp.child("penClr").and_then(dml::color) - && let slidecraft_model::ColorBase::Rgb { rgb } = c.base + && let deckcraft_model::ColorBase::Rgb { rgb } = c.base { s.pen_color = rgb; } @@ -677,7 +677,7 @@ fn header_footer(p: &Presentation) -> HeaderFooter { Some(PhType::Date) => { hf.date = true; if let Some(t) = &sh.text - && !t.paragraphs.iter().flat_map(|p| &p.runs).any(|r| matches!(r.kind, slidecraft_model::text::RunKind::Field { .. })) + && !t.paragraphs.iter().flat_map(|p| &p.runs).any(|r| matches!(r.kind, deckcraft_model::text::RunKind::Field { .. })) && hf.date_text.is_empty() { hf.date_text = t.text(); diff --git a/crates/pptx/src/read/shapes.rs b/crates/pptx/src/read/shapes.rs index 82d0155..2e10e36 100644 --- a/crates/pptx/src/read/shapes.rs +++ b/crates/pptx/src/read/shapes.rs @@ -3,12 +3,12 @@ use std::collections::{HashMap, HashSet}; -use slidecraft_color::SchemeSlot; -use slidecraft_geom::{Xfrm, preset}; -use slidecraft_model::style::{ColorRef, Effects, Fill}; -use slidecraft_model::table::{Cell, Row, Table}; -use slidecraft_model::text::{Anchor, TextDir}; -use slidecraft_model::{Geom, MediaClip, PhType, Placeholder, Shape, ShapeId, ShapeKind, ShapeStyle}; +use deckcraft_color::SchemeSlot; +use deckcraft_geom::{Xfrm, preset}; +use deckcraft_model::style::{ColorRef, Effects, Fill}; +use deckcraft_model::table::{Cell, Row, Table}; +use deckcraft_model::text::{Anchor, TextDir}; +use deckcraft_model::{Geom, MediaClip, PhType, Placeholder, Shape, ShapeId, ShapeKind, ShapeStyle}; use super::{Imp, Part, dml}; use crate::xml::El; @@ -188,7 +188,7 @@ fn sp_pr(imp: &mut Imp, part: &Part, s: &mut Shape, pr: &El) { if let Some(pg) = pr.child("prstGeom") { s.geom = preset_geom(pg); } else if let Some(cg) = pr.child("custGeom") { - let (w, h) = s.xfrm.map(|x| (slidecraft_geom::pt_to_emu(x.w) as f64, slidecraft_geom::pt_to_emu(x.h) as f64)).unwrap_or((21600.0, 21600.0)); + let (w, h) = s.xfrm.map(|x| (deckcraft_geom::pt_to_emu(x.w) as f64, deckcraft_geom::pt_to_emu(x.h) as f64)).unwrap_or((21600.0, 21600.0)); let paths = crate::custgeom::read_cust_geom(cg, w, h); if !paths.is_empty() { s.geom = Geom::Custom { paths }; diff --git a/crates/pptx/src/read/theme.rs b/crates/pptx/src/read/theme.rs index 66c15e9..5c3aa74 100644 --- a/crates/pptx/src/read/theme.rs +++ b/crates/pptx/src/read/theme.rs @@ -1,8 +1,8 @@ //! Theme parts (`a:theme`). -use slidecraft_color::{ColorScheme, Rgba, SchemeSlot}; -use slidecraft_model::style::{ColorBase, Effects}; -use slidecraft_model::theme::{FontScheme, FontSet, FormatScheme, Theme, default_format}; +use deckcraft_color::{ColorScheme, Rgba, SchemeSlot}; +use deckcraft_model::style::{ColorBase, Effects}; +use deckcraft_model::theme::{FontScheme, FontSet, FormatScheme, Theme, default_format}; use super::{Imp, Part, dml}; use crate::xml::El; @@ -22,7 +22,7 @@ pub fn read_theme(imp: &mut Imp, part: &Part, root: &El) -> Theme { let rgb = match c.base { ColorBase::Rgb { rgb } => rgb, ColorBase::System { last, .. } => last, - ColorBase::Preset { name } => slidecraft_color::preset(&name).unwrap_or(Rgba::BLACK), + ColorBase::Preset { name } => deckcraft_color::preset(&name).unwrap_or(Rgba::BLACK), ColorBase::Scheme { .. } => continue, }; scheme.set(slot, rgb); diff --git a/crates/pptx/src/read/timing.rs b/crates/pptx/src/read/timing.rs index 1efe38a..500bb48 100644 --- a/crates/pptx/src/read/timing.rs +++ b/crates/pptx/src/read/timing.rs @@ -1,7 +1,7 @@ //! Slide transitions (`p:transition`) and animations (`p:timing`). -use slidecraft_model::Shape; -use slidecraft_model::anim::{AnimClass, AnimStart, Animation, TextBuild, Transition}; +use deckcraft_model::Shape; +use deckcraft_model::anim::{AnimClass, AnimStart, Animation, TextBuild, Transition}; use super::shapes::IdCtx; use super::{Imp, Part, dml}; @@ -32,7 +32,7 @@ pub fn transition(imp: &mut Imp, part: &Part, t: &El) -> Option { None => tr.kind = "none".into(), }, } - let default_dur = slidecraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == tr.kind).map(|x| x.3).unwrap_or(1000); + let default_dur = deckcraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == tr.kind).map(|x| x.3).unwrap_or(1000); tr.duration_ms = ms_attr(t, "dur").unwrap_or(match t.attr("spd") { Some("fast") => 500, Some("med") => 750, diff --git a/crates/pptx/src/tables.rs b/crates/pptx/src/tables.rs index b73e90a..0025bd6 100644 --- a/crates/pptx/src/tables.rs +++ b/crates/pptx/src/tables.rs @@ -1,5 +1,5 @@ //! Table styles: our style ids ↔ table style GUIDs, and `tableStyles.xml` definitions for the -//! styles we write (generated from theme accents, matching how SlideCraft draws them). +//! styles we write (generated from theme accents, matching how DeckCraft draws them). use crate::opc::NS_A; use crate::xml::{A, W}; @@ -44,7 +44,7 @@ pub fn guid(text: &str) -> String { } fn own_guid(style: &str) -> String { - guid(&format!("slidecraft-table-style:{style}")) + guid(&format!("deckcraft-table-style:{style}")) } /// Table style GUID → our style id (or the GUID itself when unknown). @@ -53,7 +53,7 @@ pub fn style_from_guid(g: &str) -> String { if let Some((_, s)) = KNOWN.iter().find(|(k, _)| *k == up) { return s.to_string(); } - for (id, _) in slidecraft_model::table::table_styles() { + for (id, _) in deckcraft_model::table::table_styles() { if own_guid(&id) == up { return id; } @@ -71,7 +71,7 @@ pub fn guid_for_style(style: &str) -> (String, bool) { { return (g.to_string(), false); } - let known = slidecraft_model::table::table_styles().iter().any(|(id, _)| id == style); + let known = deckcraft_model::table::table_styles().iter().any(|(id, _)| id == style); if known { (own_guid(style), true) } else { ("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}".into(), false) } } @@ -202,7 +202,7 @@ fn style_def(w: &mut W, id: &str, name: &str) { pub fn table_styles_xml(used: &[String]) -> Vec { let mut w = W::new(); w.open("a:tblStyleLst", A::new().a("xmlns:a", NS_A).a("def", "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}")); - let names = slidecraft_model::table::table_styles(); + let names = deckcraft_model::table::table_styles(); let mut done: Vec<&str> = vec![]; for s in used { if done.contains(&s.as_str()) { @@ -213,7 +213,7 @@ pub fn table_styles_xml(used: &[String]) -> Vec { if !define { continue; } - let name = names.iter().find(|(id, _)| id == s).map(|(_, n)| format!("SlideCraft {n}")).unwrap_or_else(|| s.clone()); + let name = names.iter().find(|(id, _)| id == s).map(|(_, n)| format!("DeckCraft {n}")).unwrap_or_else(|| s.clone()); style_def(&mut w, s, &name); } w.close("a:tblStyleLst"); @@ -230,7 +230,7 @@ mod tests { "tblBg", "wholeTbl", "band1H", "band2H", "band1V", "band2V", "lastCol", "firstCol", "lastRow", "seCell", "swCell", "firstRow", "neCell", "nwCell", ]; - let used: Vec = slidecraft_model::table::table_styles().into_iter().map(|(id, _)| id).collect(); + let used: Vec = deckcraft_model::table::table_styles().into_iter().map(|(id, _)| id).collect(); let d = crate::xml::parse(&table_styles_xml(&used)).unwrap(); let mut n = 0; for st in d.root.children_named("tblStyle") { @@ -243,7 +243,7 @@ mod tests { #[test] fn guids_round_trip() { - for (id, _) in slidecraft_model::table::table_styles() { + for (id, _) in deckcraft_model::table::table_styles() { let (g, _) = guid_for_style(&id); assert_eq!(style_from_guid(&g), id, "{id} via {g}"); assert_eq!(g.len(), 38); diff --git a/crates/pptx/src/write/chart.rs b/crates/pptx/src/write/chart.rs index f8d4bf3..d832c34 100644 --- a/crates/pptx/src/write/chart.rs +++ b/crates/pptx/src/write/chart.rs @@ -1,7 +1,7 @@ //! Chart parts: a `c:chartSpace` with cached categories and values (no embedded workbook). -use slidecraft_model::chart::{Chart, ChartType, Series}; -use slidecraft_model::style::Fill; +use deckcraft_model::chart::{Chart, ChartType, Series}; +use deckcraft_model::style::Fill; use crate::opc::{NS_A, NS_C, NS_R}; use crate::xml::{A, W}; @@ -36,7 +36,7 @@ fn simple_fill(w: &mut W, f: &Fill) { } } -fn sp_pr(w: &mut W, fill: Option<&Fill>, line: Option<&slidecraft_model::Line>) { +fn sp_pr(w: &mut W, fill: Option<&Fill>, line: Option<&deckcraft_model::Line>) { if fill.is_none() && line.is_none() { return; } diff --git a/crates/pptx/src/write/dml.rs b/crates/pptx/src/write/dml.rs index 360649d..eddb618 100644 --- a/crates/pptx/src/write/dml.rs +++ b/crates/pptx/src/write/dml.rs @@ -1,8 +1,8 @@ //! DrawingML writing: colours, fills, lines, effects, transforms and text. -use slidecraft_geom::{Xfrm, pt_to_emu}; -use slidecraft_model::style::{ColorBase, ColorRef, Compound, Dash, Effects, Fill, GradientShape, Line, LineCap, LineJoin, PictureFill, PictureMode}; -use slidecraft_model::text::{ +use deckcraft_geom::{Xfrm, pt_to_emu}; +use deckcraft_model::style::{ColorBase, ColorRef, Compound, Dash, Effects, Fill, GradientShape, Line, LineCap, LineJoin, PictureFill, PictureMode}; +use deckcraft_model::text::{ Action, AutoFit, BodyProps, Bullet, Caps, Hyperlink, ListStyle, ParaProps, Paragraph, RunKind, RunProps, Spacing, Strike, TextBody, }; @@ -42,7 +42,7 @@ pub fn color(w: &mut W, c: &ColorRef) { } }; let rgb_alpha = match &c.base { - ColorBase::Rgb { rgb } if rgb.a < 255 && !c.mods.iter().any(|m| matches!(m, slidecraft_color::ColorTransform::Alpha(_))) => { + ColorBase::Rgb { rgb } if rgb.a < 255 && !c.mods.iter().any(|m| matches!(m, deckcraft_color::ColorTransform::Alpha(_))) => { Some((rgb.a as i64 * 100_000 / 255) as i32) } _ => None, @@ -81,7 +81,7 @@ pub fn fill(w: &mut W, x: &mut Exp, o: &mut Out, f: &Fill) { w.open0("a:gsLst"); let mut stops = g.stops.clone(); if stops.is_empty() { - stops.push(slidecraft_model::style::GradientStop { pos: 0.0, color: ColorRef::rgb(slidecraft_color::Rgba::WHITE) }); + stops.push(deckcraft_model::style::GradientStop { pos: 0.0, color: ColorRef::rgb(deckcraft_color::Rgba::WHITE) }); } if stops.len() == 1 { let mut s = stops[0].clone(); @@ -142,7 +142,7 @@ pub fn blip_fill(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, pf: &PictureFil color(w, &ColorRef::rgb(c)); w.close("a:clrFrom"); w.open0("a:clrTo"); - color(w, &ColorRef::rgb(c).with(slidecraft_color::ColorTransform::Alpha(0))); + color(w, &ColorRef::rgb(c).with(deckcraft_color::ColorTransform::Alpha(0))); w.close("a:clrTo"); w.close("a:clrChange"); } @@ -249,7 +249,7 @@ pub fn line(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, l: &Line) { w.close(tag); } -fn shadow(w: &mut W, tag: &str, s: &slidecraft_model::style::Shadow) { +fn shadow(w: &mut W, tag: &str, s: &deckcraft_model::style::Shadow) { let mut a = A::new().a("blurRad", emu(s.blur).max(0)).a("dist", emu(s.dist).max(0)).a("dir", ang(s.dir)); if tag == "a:outerShdw" { if s.sx != 1.0 { diff --git a/crates/pptx/src/write/mod.rs b/crates/pptx/src/write/mod.rs index 2e0ee98..5d15246 100644 --- a/crates/pptx/src/write/mod.rs +++ b/crates/pptx/src/write/mod.rs @@ -9,9 +9,9 @@ mod timing; use std::collections::HashMap; use std::sync::Arc; -use slidecraft_geom::Xfrm; -use slidecraft_model::text::{Paragraph, TextBody}; -use slidecraft_model::{ +use deckcraft_geom::Xfrm; +use deckcraft_model::text::{Paragraph, TextBody}; +use deckcraft_model::{ Background, Comment, Layout, LayoutId, Master, MediaId, PhType, Placeholder, Presentation, Shape, ShapeId, Slide, SlideId, defaults, }; @@ -158,7 +158,7 @@ impl Exp<'_> { }; o.rels.add("image", &opc::relative(&o.name, &part), false) } - pub fn chart_rel(&mut self, o: &mut Out, c: &slidecraft_model::Chart) -> String { + pub fn chart_rel(&mut self, o: &mut Out, c: &deckcraft_model::Chart) -> String { self.n_chart += 1; let name = format!("ppt/charts/chart{}.xml", self.n_chart); self.pkg.xml_part(&name, CT_CHART, chart::chart_xml(c)); @@ -178,7 +178,7 @@ fn background(w: &mut W, x: &mut Exp, o: &mut Out, bg: &Background) { Background::Fill { fill } => { w.open0("p:bgPr"); match fill { - slidecraft_model::Fill::Background | slidecraft_model::Fill::Group => w.empty0("a:noFill"), + deckcraft_model::Fill::Background | deckcraft_model::Fill::Group => w.empty0("a:noFill"), f => dml::fill(w, x, o, f), } w.empty0("a:effectLst"); @@ -233,7 +233,7 @@ fn clr_map(w: &mut W, map: &[(String, String)]) { .iter() .find(|(mk, _)| mk == k) .map(|(_, v)| v.as_str()) - .filter(|v| slidecraft_color::SchemeSlot::from_xml(v).is_some_and(|s| slidecraft_color::SchemeSlot::THEME.contains(&s))) + .filter(|v| deckcraft_color::SchemeSlot::from_xml(v).is_some_and(|s| deckcraft_color::SchemeSlot::THEME.contains(&s))) .unwrap_or(d); a = a.a(k, v); } @@ -266,7 +266,7 @@ fn ext_children(raw: Option<&str>) -> Option { Some(raw.to_string()) } -fn notes_master_shapes(size: slidecraft_geom::Size) -> Vec { +fn notes_master_shapes(size: deckcraft_geom::Size) -> Vec { let (w, h) = (size.width, size.height); let mk = |id: u32, name: &str, kind: PhType, idx: u32, x: f64, y: f64, ww: f64, hh: f64| Shape { id: ShapeId(id), @@ -277,8 +277,8 @@ fn notes_master_shapes(size: slidecraft_geom::Size) -> Vec { ..Default::default() }; let mut img = mk(4, "Slide Image Placeholder 3", PhType::SlideImage, 2, w * 0.125, h * 0.125, w * 0.75, h * 0.375); - img.fill = Some(slidecraft_model::Fill::None); - img.line = Some(slidecraft_model::Line::solid(slidecraft_model::ColorRef::rgb(slidecraft_color::Rgba::BLACK), 1.0)); + img.fill = Some(deckcraft_model::Fill::None); + img.line = Some(deckcraft_model::Line::solid(deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::BLACK), 1.0)); let mut body = mk(5, "Notes Placeholder 4", PhType::Body, 3, w * 0.1, h * 0.53, w * 0.8, h * 0.36); if let Some(t) = body.text.as_mut() { t.paragraphs = vec![Paragraph::new("Click to edit Master text styles")]; @@ -293,21 +293,21 @@ fn notes_master_shapes(size: slidecraft_geom::Size) -> Vec { ] } -fn notes_style() -> slidecraft_model::text::ListStyle { - let mut ls = slidecraft_model::text::ListStyle::default(); +fn notes_style() -> deckcraft_model::text::ListStyle { + let mut ls = deckcraft_model::text::ListStyle::default(); for i in 0..9u8 { ls.set( i, - slidecraft_model::text::LevelStyle { - para: slidecraft_model::text::ParaProps { + deckcraft_model::text::LevelStyle { + para: deckcraft_model::text::ParaProps { margin_left: Some(36.0 * i as f64), - align: Some(slidecraft_model::text::Align::Left), + align: Some(deckcraft_model::text::Align::Left), ..Default::default() }, - run: slidecraft_model::text::RunProps { + run: deckcraft_model::text::RunProps { size: Some(12.0), font: Some("+mn-lt".into()), - fill: Some(slidecraft_model::Fill::solid(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Tx1))), + fill: Some(deckcraft_model::Fill::solid(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Tx1))), ..Default::default() }, }, @@ -344,7 +344,7 @@ fn sanitized(p: &Presentation) -> std::borrow::Cow<'_, Presentation> { Arc::make_mut(m).layouts.push(Layout { id, name: "Blank".into(), - kind: slidecraft_model::LayoutType::Blank, + kind: deckcraft_model::LayoutType::Blank, show_master_shapes: true, ..Default::default() }); @@ -434,7 +434,7 @@ pub fn export(p: &Presentation) -> Result, PptxError> { x.pkg.rels(&tname, &to.rels); let mut w = W::new(); root_open(&mut w, "p:notesMaster", A::new()); - let bg = Background::Ref { idx: 1001, color: slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Bg1) }; + let bg = Background::Ref { idx: 1001, color: deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1) }; csld(&mut w, &mut x, &mut no, "", Some(&bg), ¬es_master_shapes(p.notes_size)); clr_map(&mut w, &[]); dml::list_style(&mut w, &mut x, &mut no, "p:notesStyle", ¬es_style()); @@ -542,7 +542,7 @@ fn master_xml(x: &mut Exp, o: &mut Out, m: &Master, layouts: &[(u64, String)]) - w.close("p:sldLayoutIdLst"); w.open0("p:txStyles"); let (dt, db, dother) = defaults::master_styles(); - let pick = |s: &slidecraft_model::text::ListStyle, d: slidecraft_model::text::ListStyle| if s.is_empty() { d } else { s.clone() }; + let pick = |s: &deckcraft_model::text::ListStyle, d: deckcraft_model::text::ListStyle| if s.is_empty() { d } else { s.clone() }; dml::list_style(&mut w, x, o, "p:titleStyle", &pick(&m.title_style, dt)); dml::list_style(&mut w, x, o, "p:bodyStyle", &pick(&m.body_style, db)); dml::list_style(&mut w, x, o, "p:otherStyle", &pick(&m.other_style, dother)); @@ -815,7 +815,7 @@ fn app_xml(p: &Presentation) -> Vec { .a("xmlns", "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties") .a("xmlns:vt", "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"), ); - w.elt("Application", "SlideCraft"); + w.elt("Application", "DeckCraft"); w.elt("Slides", &p.slides.len().to_string()); w.elt("Notes", &p.slides.iter().filter(|s| !s.notes.is_empty()).count().to_string()); w.elt("HiddenSlides", &p.slides.iter().filter(|s| s.hidden).count().to_string()); diff --git a/crates/pptx/src/write/shapes.rs b/crates/pptx/src/write/shapes.rs index 2910c12..9b7309c 100644 --- a/crates/pptx/src/write/shapes.rs +++ b/crates/pptx/src/write/shapes.rs @@ -2,10 +2,10 @@ use std::collections::{HashMap, HashSet}; -use slidecraft_geom::Xfrm; -use slidecraft_model::style::Fill; -use slidecraft_model::table::Table; -use slidecraft_model::{Geom, PhType, Shape, ShapeId, ShapeKind, ShapeStyle, walk}; +use deckcraft_geom::Xfrm; +use deckcraft_model::style::Fill; +use deckcraft_model::table::Table; +use deckcraft_model::{Geom, PhType, Shape, ShapeId, ShapeKind, ShapeStyle, walk}; use super::dml::{self, emu, emu_pos}; use super::{Exp, Out}; @@ -94,7 +94,7 @@ pub fn geometry(w: &mut W, g: &Geom) { w.empty0("a:avLst"); } else { // PowerPoint wants the complete guide list once any is given. - let defaults = slidecraft_geom::preset::info(name).map(|i| i.defaults).unwrap_or(&[]); + let defaults = deckcraft_geom::preset::info(name).map(|i| i.defaults).unwrap_or(&[]); let mut vals: Vec = adj.iter().take(16).copied().collect(); while vals.len() < defaults.len() { vals.push(defaults.get(vals.len()).copied().unwrap_or(0.0)); @@ -384,7 +384,7 @@ pub fn shape(w: &mut W, x: &mut Exp, o: &mut Out, cx: &mut Ctx, s: &Shape, depth } } -fn ink_shape(st: &slidecraft_model::InkStroke, i: usize) -> Option { +fn ink_shape(st: &deckcraft_model::InkStroke, i: usize) -> Option { let pts: Vec<(f64, f64)> = st.points.iter().filter(|p| p.0.is_finite() && p.1.is_finite()).map(|p| (p.0, p.1)).collect(); let (x0, y0) = pts.iter().fold((f64::MAX, f64::MAX), |a, p| (a.0.min(p.0), a.1.min(p.1))); let (x1, y1) = pts.iter().fold((f64::MIN, f64::MIN), |a, p| (a.0.max(p.0), a.1.max(p.1))); @@ -397,20 +397,20 @@ fn ink_shape(st: &slidecraft_model::InkStroke, i: usize) -> Option { d.push_str(if k == 0 { "M " } else { "L " }); d.push_str(&format!("{:.2} {:.2} ", px - x0, py - y0)); } - let mut c = slidecraft_model::style::ColorRef::rgb(st.color.with_alpha(255)); + let mut c = deckcraft_model::style::ColorRef::rgb(st.color.with_alpha(255)); if st.highlighter { - c = c.with(slidecraft_color::ColorTransform::Alpha(50_000)); + c = c.with(deckcraft_color::ColorTransform::Alpha(50_000)); } Some(Shape { id: ShapeId(0), name: format!("Ink {}", i + 1), xfrm: Some(Xfrm::new(x0, y0, w, h)), - geom: Geom::Custom { paths: vec![slidecraft_model::CustomPath { w, h, d, fill: slidecraft_geom::preset::FillMode::None, stroke: true }] }, + geom: Geom::Custom { paths: vec![deckcraft_model::CustomPath { w, h, d, fill: deckcraft_geom::preset::FillMode::None, stroke: true }] }, fill: Some(Fill::None), - line: Some(slidecraft_model::Line { - cap: Some(slidecraft_model::style::LineCap::Round), - join: Some(slidecraft_model::style::LineJoin::Round), - ..slidecraft_model::Line::solid(c, st.width.max(0.25)) + line: Some(deckcraft_model::Line { + cap: Some(deckcraft_model::style::LineCap::Round), + join: Some(deckcraft_model::style::LineJoin::Round), + ..deckcraft_model::Line::solid(c, st.width.max(0.25)) }), ..Default::default() }) @@ -520,7 +520,7 @@ fn table(w: &mut W, x: &mut Exp, o: &mut Out, t: &Table) { a = a.o("vert", c.vertical.map(|v| v.xml())).o( "anchor", c.anchor.map(|a| match a { - slidecraft_model::text::Anchor::Justified | slidecraft_model::text::Anchor::Distributed => "t", + deckcraft_model::text::Anchor::Justified | deckcraft_model::text::Anchor::Distributed => "t", other => other.xml(), }), ); @@ -551,7 +551,7 @@ fn table(w: &mut W, x: &mut Exp, o: &mut Out, t: &Table) { w.close("a:tbl"); } -fn media(w: &mut W, x: &mut Exp, o: &mut Out, id: u32, s: &Shape, m: &slidecraft_model::MediaClip) { +fn media(w: &mut W, x: &mut Exp, o: &mut Out, id: u32, s: &Shape, m: &deckcraft_model::MediaClip) { let kind = if m.video { "video" } else { "audio" }; let link = x.media_rel(o, m.media, kind); let embed = x.media_rel_new(o, m.media, crate::opc::RT_MEDIA); @@ -559,8 +559,8 @@ fn media(w: &mut W, x: &mut Exp, o: &mut Out, id: u32, s: &Shape, m: &slidecraft w.open0("p:nvPicPr"); let mut s2 = s.clone(); if s2.click.is_none() { - s2.click = Some(slidecraft_model::text::Hyperlink { - action: slidecraft_model::text::Action::PlayMedia, + s2.click = Some(deckcraft_model::text::Hyperlink { + action: deckcraft_model::text::Action::PlayMedia, tooltip: String::new(), highlight_click: false, }); @@ -603,7 +603,7 @@ fn media(w: &mut W, x: &mut Exp, o: &mut Out, id: u32, s: &Shape, m: &slidecraft } w.close("p:nvPr"); w.close("p:nvPicPr"); - let poster = slidecraft_model::style::PictureFill { media: m.poster.unwrap_or_default(), ..Default::default() }; + let poster = deckcraft_model::style::PictureFill { media: m.poster.unwrap_or_default(), ..Default::default() }; dml::blip_fill(w, x, o, "p:blipFill", &poster); let mut s3 = s.clone(); s3.fill = None; @@ -612,7 +612,7 @@ fn media(w: &mut W, x: &mut Exp, o: &mut Out, id: u32, s: &Shape, m: &slidecraft } #[allow(clippy::too_many_arguments)] -fn opaque(w: &mut W, x: &mut Exp, o: &mut Out, cx: &mut Ctx, s: &Shape, xml: &str, preview: Option, label: &str) { +fn opaque(w: &mut W, x: &mut Exp, o: &mut Out, cx: &mut Ctx, s: &Shape, xml: &str, preview: Option, label: &str) { // Kept XML that references no relationships can be written back as it was. if let Ok(doc) = crate::xml::parse(xml.as_bytes()) && !crate::read::has_rel_refs(&doc.root) @@ -632,7 +632,7 @@ fn opaque(w: &mut W, x: &mut Exp, o: &mut Out, cx: &mut Ctx, s: &Shape, xml: &st } match preview { Some(m) if x.p.media(m).is_some() => { - p.kind = ShapeKind::Picture { fill: slidecraft_model::style::PictureFill { media: m, ..Default::default() } }; + p.kind = ShapeKind::Picture { fill: deckcraft_model::style::PictureFill { media: m, ..Default::default() } }; p.ph = None; shape_with_id(w, x, o, cx, &p, id); } diff --git a/crates/pptx/src/write/theme.rs b/crates/pptx/src/write/theme.rs index 448f3f4..9b1be41 100644 --- a/crates/pptx/src/write/theme.rs +++ b/crates/pptx/src/write/theme.rs @@ -1,8 +1,8 @@ //! Theme parts. -use slidecraft_color::SchemeSlot; -use slidecraft_model::style::{ColorRef, Effects, Fill}; -use slidecraft_model::theme::{FontSet, Theme, default_format}; +use deckcraft_color::SchemeSlot; +use deckcraft_model::style::{ColorRef, Effects, Fill}; +use deckcraft_model::theme::{FontSet, Theme, default_format}; use super::{Exp, Out, dml}; use crate::opc::{NS_A, NS_R}; diff --git a/crates/pptx/src/write/timing.rs b/crates/pptx/src/write/timing.rs index e390a53..aa577ca 100644 --- a/crates/pptx/src/write/timing.rs +++ b/crates/pptx/src/write/timing.rs @@ -1,7 +1,7 @@ //! Transitions and the animation timing tree. -use slidecraft_model::anim::{AnimClass, AnimStart, Animation, TextBuild, Transition}; -use slidecraft_model::{Shape, ShapeKind, find_shape}; +use deckcraft_model::anim::{AnimClass, AnimStart, Animation, TextBuild, Transition}; +use deckcraft_model::{Shape, ShapeKind, find_shape}; use super::shapes::IdMap; use super::{Exp, Out}; @@ -360,7 +360,7 @@ fn behaviors(w: &mut W, x: &mut Exp, ids: &mut Ids, e: &Eff) { "lineColor" => "stroke.color", _ => "fillcolor", }; - let c = a.color.clone().unwrap_or(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Accent2)); + let c = a.color.clone().unwrap_or(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Accent2)); w.open("p:animClr", A::new().a("clrSpc", "rgb").a("dir", "cw")); cbhvr(w, ids, e, dur, None, &[attr], None); w.open0("p:to"); diff --git a/crates/pptx/tests/malformed.rs b/crates/pptx/tests/malformed.rs index 94f1152..e76e320 100644 --- a/crates/pptx/tests/malformed.rs +++ b/crates/pptx/tests/malformed.rs @@ -2,7 +2,7 @@ use std::io::{Cursor, Write}; -use slidecraft_model::defaults; +use deckcraft_model::defaults; fn zip_of(files: &[(&str, &[u8])]) -> Vec { let mut z = zip::ZipWriter::new(Cursor::new(Vec::new())); @@ -15,7 +15,7 @@ fn zip_of(files: &[(&str, &[u8])]) -> Vec { } fn valid() -> Vec { - slidecraft_pptx::export(&defaults::new_presentation(None)).expect("export") + deckcraft_pptx::export(&defaults::new_presentation(None)).expect("export") } /// Every entry of a valid export, as (name, bytes). @@ -38,8 +38,8 @@ fn rezip(es: &[(String, Vec)]) -> Vec { #[test] fn random_bytes_and_empty() { - assert!(slidecraft_pptx::import(&[]).is_err()); - assert!(slidecraft_pptx::import(b"PK\x03\x04garbage").is_err()); + assert!(deckcraft_pptx::import(&[]).is_err()); + assert!(deckcraft_pptx::import(b"PK\x03\x04garbage").is_err()); let mut seed = 7u64; for len in [1usize, 10, 100, 1000, 10_000] { let v: Vec = (0..len) @@ -48,7 +48,7 @@ fn random_bytes_and_empty() { (seed >> 33) as u8 }) .collect(); - assert!(slidecraft_pptx::import(&v).is_err()); + assert!(deckcraft_pptx::import(&v).is_err()); } } @@ -56,14 +56,14 @@ fn random_bytes_and_empty() { fn truncated_zip() { let v = valid(); for cut in [10, v.len() / 4, v.len() / 2, v.len() - 30, v.len() - 1] { - let _ = slidecraft_pptx::import(&v[..cut]); + let _ = deckcraft_pptx::import(&v[..cut]); } } #[test] fn zip_without_presentation() { let z = zip_of(&[("[Content_Types].xml", b""), ("hello.txt", b"hi")]); - assert!(slidecraft_pptx::import(&z).is_err()); + assert!(deckcraft_pptx::import(&z).is_err()); } #[test] @@ -73,13 +73,13 @@ fn missing_parts_still_import() { for i in 0..es.len() { let mut v = es.clone(); let (name, _) = v.remove(i); - let r = slidecraft_pptx::import(&rezip(&v)); + let r = deckcraft_pptx::import(&rezip(&v)); if name == "ppt/presentation.xml" { assert!(r.is_err()); } else if let Ok(p) = r { assert!(p.validate().is_empty(), "dropping {name}: {:?}", p.validate()); // Whatever we read must export again. - slidecraft_pptx::export(&p).expect("re-export"); + deckcraft_pptx::export(&p).expect("re-export"); } } } @@ -91,8 +91,8 @@ fn bad_xml_in_each_part() { for bad in [&b"", b""] { let mut v = es.clone(); v[i].1 = bad.to_vec(); - if let Ok(p) = slidecraft_pptx::import(&rezip(&v)) { - slidecraft_pptx::export(&p).expect("re-export"); + if let Ok(p) = deckcraft_pptx::import(&rezip(&v)) { + deckcraft_pptx::export(&p).expect("re-export"); } } } @@ -114,15 +114,15 @@ fn huge_numbers_and_nonsense_attributes() { *d = s.into_bytes(); } } - if let Ok(p) = slidecraft_pptx::import(&rezip(&v)) { + if let Ok(p) = deckcraft_pptx::import(&rezip(&v)) { for s in &p.slides { - slidecraft_model::walk(&s.shapes, &mut |sh, _| { + deckcraft_model::walk(&s.shapes, &mut |sh, _| { if let Some(x) = sh.xfrm { assert!(x.is_finite()); } }); } - slidecraft_pptx::export(&p).expect("re-export"); + deckcraft_pptx::export(&p).expect("re-export"); } } @@ -138,14 +138,14 @@ fn deep_nesting_is_bounded() { if let Some(e) = v.iter_mut().find(|(n, _)| n == "ppt/slides/slide1.xml") { e.1 = deep.into_bytes(); } - let p = slidecraft_pptx::import(&rezip(&v)).expect("import"); - slidecraft_pptx::export(&p).expect("re-export"); + let p = deckcraft_pptx::import(&rezip(&v)).expect("import"); + deckcraft_pptx::export(&p).expect("re-export"); // Beyond the XML depth limit the part is rejected, the deck still loads. let deeper = format!("{}", "".repeat(5000)); if let Some(e) = v.iter_mut().find(|(n, _)| n == "ppt/slides/slide1.xml") { e.1 = deeper.into_bytes(); } - let _ = slidecraft_pptx::import(&rezip(&v)); + let _ = deckcraft_pptx::import(&rezip(&v)); } #[test] @@ -155,7 +155,7 @@ fn relationship_cycles_and_bad_targets() { if let Some(e) = v.iter_mut().find(|(n, _)| n == "ppt/slides/_rels/slide1.xml.rels") { e.1 = br#""#.to_vec(); } - let p = slidecraft_pptx::import(&rezip(&v)).expect("import"); + let p = deckcraft_pptx::import(&rezip(&v)).expect("import"); assert_eq!(p.slides.len(), 1); assert!(p.validate().is_empty()); } @@ -164,11 +164,11 @@ fn relationship_cycles_and_bad_targets() { #[test] fn fuzz_mutations() { let p = { - let mut s = slidecraft_engine::Session::new(); - slidecraft_engine::sample::open_sample(&mut s).expect("sample"); + let mut s = deckcraft_engine::Session::new(); + deckcraft_engine::sample::open_sample(&mut s).expect("sample"); s.doc().expect("doc").doc.as_ref().clone() }; - let es = entries(&slidecraft_pptx::export(&p).expect("export")); + let es = entries(&deckcraft_pptx::export(&p).expect("export")); let mut seed = 0x5eed_u64; let mut rnd = |n: usize| { seed ^= seed << 13; @@ -198,20 +198,20 @@ fn fuzz_mutations() { } } } - if let Ok(q) = slidecraft_pptx::import(&rezip(&v)) { + if let Ok(q) = deckcraft_pptx::import(&rezip(&v)) { assert!(q.validate().is_empty()); - slidecraft_pptx::export(&q).expect("re-export"); + deckcraft_pptx::export(&q).expect("re-export"); } } // Raw byte flips of the whole zip, too. - let bytes = slidecraft_pptx::export(&p).expect("export"); + let bytes = deckcraft_pptx::export(&p).expect("export"); for _ in 0..iters { let mut b = bytes.clone(); for _ in 0..1 + rnd(16) { let at = rnd(b.len()); b[at] = rnd(256) as u8; } - let _ = slidecraft_pptx::import(&b); + let _ = deckcraft_pptx::import(&b); } } @@ -229,13 +229,13 @@ fn zip_bomb_entry_is_capped() { } let bytes = z.finish().expect("finish").into_inner(); assert!(bytes.len() < 2 * 1024 * 1024); - let p = slidecraft_pptx::import(&bytes).expect("import"); + let p = deckcraft_pptx::import(&bytes).expect("import"); assert_eq!(p.slides.len(), 1); } #[test] fn export_of_odd_models_is_valid() { - use slidecraft_model::{LayoutId, Shape, ShapeId, ShapeKind, Slide, SlideId}; + use deckcraft_model::{LayoutId, Shape, ShapeId, ShapeKind, Slide, SlideId}; use std::sync::Arc; // No masters, dangling layout, duplicate and zero shape ids, NaN geometry, control characters. let mut p = defaults::new_presentation(None); @@ -244,8 +244,8 @@ fn export_of_odd_models_is_valid() { s.shapes.push(Shape { id: ShapeId(id), name: "bad\u{0}name\u{b}".into(), - xfrm: Some(slidecraft_geom::Xfrm::new(f64::NAN, f64::INFINITY, -5.0, 1e300)), - text: Some(slidecraft_model::TextBody::from_text("tab\tand\u{1}control\nnew line")), + xfrm: Some(deckcraft_geom::Xfrm::new(f64::NAN, f64::INFINITY, -5.0, 1e300)), + text: Some(deckcraft_model::TextBody::from_text("tab\tand\u{1}control\nnew line")), ..Default::default() }); } @@ -255,12 +255,12 @@ fn export_of_odd_models_is_valid() { ..Default::default() }); p.slides.push(Arc::new(s)); - let bytes = slidecraft_pptx::export(&p).expect("export"); - let q = slidecraft_pptx::import(&bytes).expect("import"); + let bytes = deckcraft_pptx::export(&p).expect("export"); + let q = deckcraft_pptx::import(&bytes).expect("import"); assert_eq!(q.slides.len(), 2); assert!(q.validate().is_empty(), "{:?}", q.validate()); let mut empty = p.clone(); empty.masters.clear(); - let q = slidecraft_pptx::import(&slidecraft_pptx::export(&empty).expect("export")).expect("import"); + let q = deckcraft_pptx::import(&deckcraft_pptx::export(&empty).expect("export")).expect("import"); assert!(!q.masters.is_empty()); } diff --git a/crates/pptx/tests/roundtrip.rs b/crates/pptx/tests/roundtrip.rs index 409dcdb..fbe2b32 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -2,19 +2,19 @@ use std::sync::Arc; -use slidecraft_color::{ColorTransform, Rgba, SchemeSlot}; -use slidecraft_geom::Xfrm; -use slidecraft_model::anim::{AnimClass, AnimStart, Animation, TextBuild, Transition}; -use slidecraft_model::chart::{Chart, ChartType}; -use slidecraft_model::style::{ +use deckcraft_color::{ColorTransform, Rgba, SchemeSlot}; +use deckcraft_geom::Xfrm; +use deckcraft_model::anim::{AnimClass, AnimStart, Animation, TextBuild, Transition}; +use deckcraft_model::chart::{Chart, ChartType}; +use deckcraft_model::style::{ ColorRef, Dash, Effects, Fill, Glow, Gradient, GradientShape, GradientStop, Line, LineEnd, PatternFill, PictureAdjust, PictureFill, PictureMode, Shadow, }; -use slidecraft_model::table::Table; -use slidecraft_model::text::{ +use deckcraft_model::table::Table; +use deckcraft_model::text::{ Action, Align, Anchor, AutoFit, BodyProps, Bullet, Caps, Hyperlink, Paragraph, Run, RunKind, RunProps, Spacing, Strike, TextBody, }; -use slidecraft_model::{ +use deckcraft_model::{ Background, Comment, CustomPath, CustomShow, Geom, InkStroke, MediaClip, Presentation, Section, Shape, ShapeId, ShapeKind, Slide, SlideId, defaults, }; @@ -31,7 +31,7 @@ fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> Sh } fn new_slide(p: &mut Presentation, title: &str) -> Slide { - let layout = defaults::layout_of_kind(p, slidecraft_model::LayoutType::TitleOnly).unwrap_or_default(); + let layout = defaults::layout_of_kind(p, deckcraft_model::LayoutType::TitleOnly).unwrap_or_default(); let mut s = defaults::new_slide(p, layout); if let Some(t) = s.shapes.first_mut() { t.text = Some(TextBody::from_text(title)); @@ -43,7 +43,7 @@ fn new_slide(p: &mut Presentation, title: &str) -> Slide { pub fn rich_deck() -> Presentation { let mut p = defaults::new_presentation(None); p.props.title = "Rich deck".into(); - p.props.author = "SlideCraft tests".into(); + p.props.author = "DeckCraft tests".into(); p.props.created = "2025-01-02T03:04:05Z".into(); let img = p.add_media("image1.png", "image/png", png(40, 30, [200, 40, 40])); let img2 = p.add_media("image2.png", "image/png", png(8, 8, [20, 140, 220])); @@ -139,7 +139,7 @@ pub fn rich_deck() -> Presentation { b.text = Some(TextBody { body: BodyProps { anchor: Some(Anchor::Bottom), inset_l: Some(10.0), wrap: Some(false), autofit: Some(AutoFit::Shape), ..Default::default() }, paragraphs: vec![Paragraph { - props: slidecraft_model::text::ParaProps { + props: deckcraft_model::text::ParaProps { align: Some(Align::Right), line_spacing: Some(Spacing::Pct(1.5)), space_before: Some(Spacing::Pts(6.0)), @@ -159,7 +159,7 @@ pub fn rich_deck() -> Presentation { w: 100000.0, h: 100000.0, d: "M 0 0 L 100000 0 L 50000 100000 Z".into(), - fill: slidecraft_geom::preset::FillMode::Norm, + fill: deckcraft_geom::preset::FillMode::Norm, stroke: true, }], }; @@ -325,7 +325,7 @@ pub fn rich_deck() -> Presentation { p.slides.push(Arc::new(s)); // Slide 6: bulleted content with a by-paragraph build, a slide link and an equation. - let layout = defaults::layout_of_kind(&p, slidecraft_model::LayoutType::TitleAndContent).unwrap_or_default(); + let layout = defaults::layout_of_kind(&p, deckcraft_model::LayoutType::TitleAndContent).unwrap_or_default(); let mut s = defaults::new_slide(&mut p, layout); s.shapes[0].text = Some(TextBody::from_text("Build")); s.shapes[1].text = Some(TextBody::from_text("One\nTwo\nThree")); @@ -361,13 +361,13 @@ pub fn rich_deck() -> Presentation { } fn round(p: &Presentation) -> Presentation { - let bytes = slidecraft_pptx::export(p).expect("export"); - slidecraft_pptx::import(&bytes).expect("import") + let bytes = deckcraft_pptx::export(p).expect("export"); + deckcraft_pptx::import(&bytes).expect("import") } fn by_name<'a>(s: &'a Slide, name: &str) -> &'a Shape { let mut found = None; - slidecraft_model::walk(&s.shapes, &mut |x, _| { + deckcraft_model::walk(&s.shapes, &mut |x, _| { if found.is_none() && x.name == name { found = Some(x); } @@ -378,8 +378,8 @@ fn by_name<'a>(s: &'a Slide, name: &str) -> &'a Shape { #[test] fn rich_deck_writes_to_dir() { let p = rich_deck(); - let bytes = slidecraft_pptx::export(&p).expect("export"); - if let Ok(dir) = std::env::var("SLIDECRAFT_PPTX_OUT") { + let bytes = deckcraft_pptx::export(&p).expect("export"); + if let Ok(dir) = std::env::var("DECKCRAFT_PPTX_OUT") { std::fs::write(format!("{dir}/rich.pptx"), &bytes).expect("write"); } assert!(bytes.len() > 1000); @@ -394,7 +394,7 @@ fn deck_level_round_trip() { assert_eq!(q.slide_size, p.slide_size); assert_eq!(q.first_slide_number, 0); assert_eq!(q.props.title, "Rich deck"); - assert_eq!(q.props.author, "SlideCraft tests"); + assert_eq!(q.props.author, "DeckCraft tests"); assert_eq!(q.props.created, "2025-01-02T03:04:05Z"); assert_eq!( q.sections.iter().map(|s| (s.name.clone(), s.slides.len())).collect::>(), @@ -585,7 +585,7 @@ fn every_transition_kind_survives() { let mut p = defaults::new_presentation(None); let base = p.slides[0].as_ref().clone(); p.slides.clear(); - for (kind, ..) in slidecraft_model::anim::TRANSITIONS { + for (kind, ..) in deckcraft_model::anim::TRANSITIONS { let mut s = base.clone(); s.id = SlideId(p.alloc_id()); s.transition = Some(Transition { kind: kind.to_string(), duration_ms: 1200, ..Default::default() }); @@ -610,7 +610,7 @@ fn every_animation_effect_survives() { sh.text = Some(TextBody::from_text("hi")); let id = sh.id; s.shapes.push(sh); - for (effect, _, class, _, dur, opts) in slidecraft_model::anim::ANIMATIONS { + for (effect, _, class, _, dur, opts) in deckcraft_model::anim::ANIMATIONS { s.animations.push(Animation { shape: id, class: *class, @@ -626,17 +626,17 @@ fn every_animation_effect_survives() { assert_eq!(a.len(), b.len()); for (x, y) in a.iter().zip(b.iter()) { // Effects sharing a preset number come back as the first effect with that number. - let same_preset = slidecraft_model::anim::animation_info(&x.effect, x.class).map(|i| i.3) - == slidecraft_model::anim::animation_info(&y.effect, y.class).map(|i| i.3); + let same_preset = deckcraft_model::anim::animation_info(&x.effect, x.class).map(|i| i.3) + == deckcraft_model::anim::animation_info(&y.effect, y.class).map(|i| i.3); assert!(x.effect == y.effect || same_preset, "{} vs {}", x.effect, y.effect); assert_eq!(x.class, y.class); } } -/// Writes bisection variants of the rich deck to `SLIDECRAFT_PPTX_BISECT` (manual checks). +/// Writes bisection variants of the rich deck to `DECKCRAFT_PPTX_BISECT` (manual checks). #[test] fn bisect_variants() { - let Ok(dir) = std::env::var("SLIDECRAFT_PPTX_BISECT") else { return }; + let Ok(dir) = std::env::var("DECKCRAFT_PPTX_BISECT") else { return }; let p = rich_deck(); let n = p.slides.len(); for i in 0..n { @@ -644,7 +644,7 @@ fn bisect_variants() { q.slides = vec![p.slides[i].clone()]; q.sections.clear(); q.custom_shows.clear(); - std::fs::write(format!("{dir}/bis-slide{i}.pptx"), slidecraft_pptx::export(&q).expect("export")).expect("write"); + std::fs::write(format!("{dir}/bis-slide{i}.pptx"), deckcraft_pptx::export(&q).expect("export")).expect("write"); } let mut q = p.clone(); q.sections.clear(); @@ -653,14 +653,14 @@ fn bisect_variants() { for s in q.slides.iter_mut() { Arc::make_mut(s).comments.clear(); } - std::fs::write(format!("{dir}/bis-nodeck.pptx"), slidecraft_pptx::export(&q).expect("export")).expect("write"); + std::fs::write(format!("{dir}/bis-nodeck.pptx"), deckcraft_pptx::export(&q).expect("export")).expect("write"); } -/// Finer bisection of one slide (`SLIDECRAFT_PPTX_BISECT_SLIDE=`): one variant per removed shape, +/// Finer bisection of one slide (`DECKCRAFT_PPTX_BISECT_SLIDE=`): one variant per removed shape, /// plus no animations / no transition. #[test] fn bisect_slide_variants() { - let (Ok(dir), Ok(idx)) = (std::env::var("SLIDECRAFT_PPTX_BISECT"), std::env::var("SLIDECRAFT_PPTX_BISECT_SLIDE")) else { return }; + let (Ok(dir), Ok(idx)) = (std::env::var("DECKCRAFT_PPTX_BISECT"), std::env::var("DECKCRAFT_PPTX_BISECT_SLIDE")) else { return }; let p = rich_deck(); let i: usize = idx.parse().expect("index"); let base = p.slides[i].as_ref().clone(); @@ -669,7 +669,7 @@ fn bisect_slide_variants() { q.slides = vec![Arc::new(s)]; q.sections.clear(); q.custom_shows.clear(); - slidecraft_pptx::export(&q).expect("export") + deckcraft_pptx::export(&q).expect("export") }; let mut s = base.clone(); s.animations.clear(); diff --git a/crates/pptx/tests/sample.rs b/crates/pptx/tests/sample.rs index 18eaa9b..f42ca23 100644 --- a/crates/pptx/tests/sample.rs +++ b/crates/pptx/tests/sample.rs @@ -1,25 +1,25 @@ -//! The engine's sample deck: export, re-import, compare. Set `SLIDECRAFT_PPTX_OUT=` to also +//! The engine's sample deck: export, re-import, compare. Set `DECKCRAFT_PPTX_OUT=` to also //! write the exported file there (for checking it in other applications). -use slidecraft_engine::Session; -use slidecraft_model::{Presentation, ShapeKind}; +use deckcraft_engine::Session; +use deckcraft_model::{Presentation, ShapeKind}; fn sample() -> Presentation { let mut s = Session::new(); - slidecraft_engine::sample::open_sample(&mut s).expect("sample"); + deckcraft_engine::sample::open_sample(&mut s).expect("sample"); s.doc().expect("doc").doc.as_ref().clone() } #[test] fn sample_deck_round_trips() { let p = sample(); - let bytes = slidecraft_pptx::export(&p).expect("export"); - if let Ok(dir) = std::env::var("SLIDECRAFT_PPTX_OUT") { + let bytes = deckcraft_pptx::export(&p).expect("export"); + if let Ok(dir) = std::env::var("DECKCRAFT_PPTX_OUT") { std::fs::create_dir_all(&dir).ok(); std::fs::write(format!("{dir}/sample.pptx"), &bytes).expect("write"); } - assert!(slidecraft_pptx::sniff(&bytes)); - let q = slidecraft_pptx::import(&bytes).expect("import"); + assert!(deckcraft_pptx::sniff(&bytes)); + let q = deckcraft_pptx::import(&bytes).expect("import"); assert_eq!(q.slides.len(), p.slides.len()); assert!(q.validate().is_empty(), "{:?}", q.validate()); for (a, b) in p.slides.iter().zip(q.slides.iter()) { @@ -62,26 +62,26 @@ fn sample_deck_round_trips() { assert_eq!(a.slides.len(), b.slides.len()); } // A second round trip is stable. - let bytes2 = slidecraft_pptx::export(&q).expect("export 2"); - let r = slidecraft_pptx::import(&bytes2).expect("import 2"); + let bytes2 = deckcraft_pptx::export(&q).expect("export 2"); + let r = deckcraft_pptx::import(&bytes2).expect("import 2"); assert_eq!(r.slides.len(), q.slides.len()); } -/// Reads any file named by `SLIDECRAFT_PPTX_IN` (for checking decks made by other applications). +/// Reads any file named by `DECKCRAFT_PPTX_IN` (for checking decks made by other applications). #[test] fn external_file_imports() { - let Ok(path) = std::env::var("SLIDECRAFT_PPTX_IN") else { return }; + let Ok(path) = std::env::var("DECKCRAFT_PPTX_IN") else { return }; let bytes = std::fs::read(&path).expect("read"); - let p = slidecraft_pptx::import(&bytes).expect("import"); + let p = deckcraft_pptx::import(&bytes).expect("import"); println!("{path}: {} slides, {} masters, {} layouts", p.slides.len(), p.masters.len(), p.masters.iter().map(|m| m.layouts.len()).sum::()); for (i, s) in p.slides.iter().enumerate() { let kinds: Vec<&str> = s.shapes.iter().map(|s| s.kind_name()).collect(); println!(" slide {}: title {:?} shapes {:?} layout {:?}", i + 1, s.title(), kinds, p.layout(s.layout).map(|(_, l)| l.name.clone())); } assert!(p.validate().is_empty()); - let again = slidecraft_pptx::import(&slidecraft_pptx::export(&p).expect("export")).expect("reimport"); + let again = deckcraft_pptx::import(&deckcraft_pptx::export(&p).expect("export")).expect("reimport"); assert_eq!(again.slides.len(), p.slides.len()); - if let Ok(dir) = std::env::var("SLIDECRAFT_PPTX_OUT") { - std::fs::write(format!("{dir}/reexport.pptx"), slidecraft_pptx::export(&p).expect("export")).expect("write"); + if let Ok(dir) = std::env::var("DECKCRAFT_PPTX_OUT") { + std::fs::write(format!("{dir}/reexport.pptx"), deckcraft_pptx::export(&p).expect("export")).expect("write"); } } diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 3dd4734..6eecf62 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -1,19 +1,19 @@ [package] -name = "slidecraft-render" +name = "deckcraft-render" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft renderer: slides, layouts and masters to pixels (vello_cpu): fills, outlines, effects, pictures, text, tables, charts" +description = "DeckCraft renderer: slides, layouts and masters to pixels (vello_cpu): fills, outlines, effects, pictures, text, tables, charts" [lints] workspace = true [dependencies] -slidecraft-geom = { workspace = true } -slidecraft-color = { workspace = true } -slidecraft-model = { workspace = true } -slidecraft-fonts = { workspace = true } -slidecraft-text = { workspace = true } +deckcraft-geom = { workspace = true } +deckcraft-color = { workspace = true } +deckcraft-model = { workspace = true } +deckcraft-fonts = { workspace = true } +deckcraft-text = { workspace = true } vello_cpu = { workspace = true } vello_common = { workspace = true } image = { workspace = true } diff --git a/crates/render/src/chart.rs b/crates/render/src/chart.rs index 059bb52..a29b202 100644 --- a/crates/render/src/chart.rs +++ b/crates/render/src/chart.rs @@ -1,12 +1,12 @@ //! Charts: title, plot area with axes and gridlines, series marks, legend. +use deckcraft_color::{ColorTransform, Rgba, SchemeSlot}; +use deckcraft_model::Shape; +use deckcraft_model::chart::{Chart, ChartType, nice_scale}; +use deckcraft_model::resolve::Ctx; +use deckcraft_model::style::{ColorRef, Fill}; +use deckcraft_model::text::{Align, Anchor, Bullet, TextBody}; use kurbo::{Affine, BezPath, Point, Rect, Shape as _}; -use slidecraft_color::{ColorTransform, Rgba, SchemeSlot}; -use slidecraft_model::Shape; -use slidecraft_model::chart::{Chart, ChartType, nice_scale}; -use slidecraft_model::resolve::Ctx; -use slidecraft_model::style::{ColorRef, Fill}; -use slidecraft_model::text::{Align, Anchor, Bullet, TextBody}; use vello_cpu::RenderContext; use crate::color; @@ -23,13 +23,13 @@ pub fn series_color(ctx: &Ctx, c: &Chart, i: usize) -> Rgba { let round = if mono { i } else { i / 6 }; match round { 0 => base, - 1 => slidecraft_color::apply(base, &[ColorTransform::Shade(60000)]), - 2 => slidecraft_color::apply(base, &[ColorTransform::Tint(60000)]), - n => slidecraft_color::apply(base, &[ColorTransform::LumMod(100000 - (n as i32 % 5) * 12000)]), + 1 => deckcraft_color::apply(base, &[ColorTransform::Shade(60000)]), + 2 => deckcraft_color::apply(base, &[ColorTransform::Tint(60000)]), + n => deckcraft_color::apply(base, &[ColorTransform::LumMod(100000 - (n as i32 % 5) * 12000)]), } } -fn series_fill(ctx: &Ctx, c: &Chart, i: usize, s: Option<&slidecraft_model::chart::Series>) -> Rgba { +fn series_fill(ctx: &Ctx, c: &Chart, i: usize, s: Option<&deckcraft_model::chart::Series>) -> Rgba { match s.and_then(|s| s.fill.as_ref()) { Some(Fill::Solid { color: cc }) => ctx.color(cc, None), _ => series_color(ctx, c, i), @@ -56,7 +56,7 @@ fn label(ctx: &mut RenderContext, rctx: &Ctx, m: Affine, text: &str, r: Rect, si } } let tmp = Shape { text: Some(body.clone()), ..Default::default() }; - crate::draw_text(ctx, rctx, &tmp, &body, r, m, &slidecraft_text::NoFields, None); + crate::draw_text(ctx, rctx, &tmp, &body, r, m, &deckcraft_text::NoFields, None); } fn fmt_num(v: f64) -> String { @@ -75,7 +75,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h &Rect::new(0.0, 0.0, w, h).to_path(0.1), Rect::new(0.0, 0.0, w, h), m, - slidecraft_geom::preset::FillMode::Norm, + deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres, ); @@ -173,7 +173,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h Rect::new(area.x0 + gutter, area.y0, area.x1, area.y1 - base * 1.8) }; if let Some(f) = &c.plot_fill { - crate::paint::fill_path(ctx, rctx, f, None, &plot.to_path(0.1), plot, m, slidecraft_geom::preset::FillMode::Norm, 1.0, rctx.pres); + crate::paint::fill_path(ctx, rctx, f, None, &plot.to_path(0.1), plot, m, deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres); } let span = (max - min).max(1e-9); let to_v = |v: f64| if horiz { plot.x0 + (v - min) / span * plot.width() } else { plot.y1 - (v - min) / span * plot.height() }; diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index 0359765..cfca026 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use slidecraft_model::style::PictureAdjust; +use deckcraft_model::style::PictureAdjust; use vello_cpu::Pixmap; const BUDGET: usize = 512 << 20; diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index cad6aa3..1d0edb0 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1,4 +1,4 @@ -//! The SlideCraft renderer: draws slides, layouts and masters with `vello_cpu`. +//! The DeckCraft renderer: draws slides, layouts and masters with `vello_cpu`. //! //! One code path serves the editor canvas, thumbnails, image/PDF-raster export and the slide show: //! [`render_slide`] draws the background, the master and layout graphics, then the slide's shapes @@ -16,15 +16,15 @@ mod table; use std::collections::HashMap; use std::sync::Arc; +use deckcraft_color::Rgba; +use deckcraft_fonts::FontDb; +use deckcraft_geom::preset::{self, FillMode, Geometry, SubPath}; +use deckcraft_model::resolve::{self, Ctx}; +use deckcraft_model::style::{Effects, Fill, Line}; +use deckcraft_model::text::TextBody; +use deckcraft_model::{Geom, PhType, Presentation, Shape, ShapeId, ShapeKind, Slide}; +use deckcraft_text::{Fields, Opts}; use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape as _, Vec2}; -use slidecraft_color::Rgba; -use slidecraft_fonts::FontDb; -use slidecraft_geom::preset::{self, FillMode, Geometry, SubPath}; -use slidecraft_model::resolve::{self, Ctx}; -use slidecraft_model::style::{Effects, Fill, Line}; -use slidecraft_model::text::TextBody; -use slidecraft_model::{Geom, PhType, Presentation, Shape, ShapeId, ShapeKind, Slide}; -use slidecraft_text::{Fields, Opts}; use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive}; use vello_cpu::{RenderContext, Resources, peniko}; @@ -208,7 +208,7 @@ pub fn needs_filters(pres: &Presentation, slide: &Slide) -> bool { let Some(rctx) = Ctx::for_slide(pres, slide) else { return false }; let mut any = false; let mut check = |shapes: &[Shape]| { - slidecraft_model::walk(shapes, &mut |s, _| { + deckcraft_model::walk(shapes, &mut |s, _| { if any { return; } @@ -498,7 +498,7 @@ impl Renderer { match &s.kind { ShapeKind::Group { children, child } => { let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h); - let gm = parent * slidecraft_geom::group_child_affine(&x, ch); + let gm = parent * deckcraft_geom::group_child_affine(&x, ch); for c in children { self.shape(ctx, f, rctx, c, gm, depth + 1); } @@ -537,7 +537,7 @@ impl Renderer { let outline = geo.outline(); // Placeholders in the editor: dashed outline and prompt, nothing else when empty. let empty_ph = s.ph.is_some() && s.text.as_ref().is_none_or(TextBody::is_empty) && !is_pic && !matches!(s.kind, ShapeKind::Media(_)); - let fill = if let Some(t) = st.tint { Some(Fill::solid(slidecraft_model::ColorRef::rgb(t))) } else { fill }; + let fill = if let Some(t) = st.tint { Some(Fill::solid(deckcraft_model::ColorRef::rgb(t))) } else { fill }; // Effects below the shape. if let Some(e) = &effects { self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, m, w, h); @@ -561,7 +561,7 @@ impl Renderer { ctx.set_transform(m); let bounds = Rect::new(0.0, 0.0, w, h); if let Some(p) = mc.poster { - let pf = slidecraft_model::style::PictureFill { media: p, ..Default::default() }; + let pf = deckcraft_model::style::PictureFill { media: p, ..Default::default() }; paint::picture(ctx, f.pres, &pf, bounds, m); } else if mc.video { ctx.set_paint(peniko::Color::from_rgba8(20, 20, 24, 255)); @@ -640,7 +640,7 @@ impl Renderer { { let tr = geo.text_rect; if !body.is_empty() { - let l = slidecraft_text::layout(rctx, s, body, &Opts { rect: tr, fields: &f.fields, prompt_color: None, no_shrink: false }); + let l = deckcraft_text::layout(rctx, s, body, &Opts { rect: tr, fields: &f.fields, prompt_color: None, no_shrink: false }); draw_layout_with(ctx, &l, tr, m, &st.paras); } else if f.opts.edit && s.ph.is_some() { let kind = s.ph_type().unwrap_or(PhType::Body); @@ -747,7 +747,7 @@ fn blur(sigma: f64) -> Filter { } fn stroke(ctx: &mut RenderContext, rctx: &Ctx, line: &Line, ph: Option, path: &BezPath, m: Affine, lw: f64) { - use slidecraft_model::style::{LineCap, LineJoin}; + use deckcraft_model::style::{LineCap, LineJoin}; let cap = match line.cap.unwrap_or_default() { LineCap::Flat => kurbo::Cap::Butt, LineCap::Round => kurbo::Cap::Round, @@ -896,10 +896,10 @@ fn prompt_body(rctx: &Ctx, s: &Shape, kind: PhType) -> TextBody { } let mut t = TextBody::from_text(txt); if matches!(kind, PhType::Picture | PhType::Chart | PhType::Table | PhType::Media | PhType::Diagram | PhType::ClipArt) { - t.body.anchor = Some(slidecraft_model::text::Anchor::Bottom); + t.body.anchor = Some(deckcraft_model::text::Anchor::Bottom); if let Some(p) = t.paragraphs.first_mut() { - p.props.align = Some(slidecraft_model::text::Align::Center); - p.props.bullet = Some(slidecraft_model::text::Bullet::None); + p.props.align = Some(deckcraft_model::text::Align::Center); + p.props.bullet = Some(deckcraft_model::text::Bullet::None); p.props.margin_left = Some(0.0); p.props.indent = Some(0.0); for r in &mut p.runs { @@ -914,7 +914,7 @@ thread_local! { static GLYPH_CACHE: std::cell::RefCell>> = std::cell::RefCell::new(HashMap::new()); } -fn glyph(face: &slidecraft_fonts::FontFace, gid: u32) -> Arc { +fn glyph(face: &deckcraft_fonts::FontFace, gid: u32) -> Arc { let key = (face.id(), gid); if let Some(p) = GLYPH_CACHE.with(|c| c.borrow().get(&key).cloned()) { return p; @@ -932,17 +932,17 @@ fn glyph(face: &slidecraft_fonts::FontFace, gid: u32) -> Arc { /// Draw a text body into shape space `m` within the text rect. pub fn draw_text(ctx: &mut RenderContext, rctx: &Ctx, s: &Shape, body: &TextBody, tr: Rect, m: Affine, fields: &dyn Fields, prompt: Option) { - let l = slidecraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: prompt, no_shrink: false }); + let l = deckcraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: prompt, no_shrink: false }); draw_layout(ctx, &l, tr, m); } /// Draw an already laid-out text block. -pub fn draw_layout(ctx: &mut RenderContext, l: &slidecraft_text::TextLayout, tr: Rect, m: Affine) { +pub fn draw_layout(ctx: &mut RenderContext, l: &deckcraft_text::TextLayout, tr: Rect, m: Affine) { draw_layout_with(ctx, l, tr, m, &[]); } /// Draw a laid-out text block with per-paragraph animation states. -pub fn draw_layout_with(ctx: &mut RenderContext, l: &slidecraft_text::TextLayout, tr: Rect, m: Affine, paras: &[(usize, ParaState)]) { +pub fn draw_layout_with(ctx: &mut RenderContext, l: &deckcraft_text::TextLayout, tr: Rect, m: Affine, paras: &[(usize, ParaState)]) { let para = |i: usize| paras.iter().find(|(p, _)| *p == i).map(|(_, s)| *s).unwrap_or_default(); let m = if l.rotation != 0.0 { let c = tr.center().to_vec2(); @@ -950,7 +950,7 @@ pub fn draw_layout_with(ctx: &mut RenderContext, l: &slidecraft_text::TextLayout } else { m }; - let deco = |ctx: &mut RenderContext, d: &slidecraft_text::Deco| { + let deco = |ctx: &mut RenderContext, d: &deckcraft_text::Deco| { let ps = para(d.para); if !ps.visible || ps.opacity <= 0.001 { return; @@ -1031,15 +1031,15 @@ mod mt_tests { /// Regression: multithreaded rendering of a slide with shadows panicked inside vello_cpu. #[test] fn effects_render_with_threads_requested() { - let mut s = slidecraft_model::Presentation::default(); + let mut s = deckcraft_model::Presentation::default(); let sl = std::sync::Arc::make_mut(&mut s.slides[0]); - let mut sh = slidecraft_model::Shape { + let mut sh = deckcraft_model::Shape { id: ShapeId(900), - xfrm: Some(slidecraft_geom::Xfrm::new(10.0, 10.0, 100.0, 100.0)), - style: Some(slidecraft_model::ShapeStyle::accent(slidecraft_color::SchemeSlot::Accent1)), + xfrm: Some(deckcraft_geom::Xfrm::new(10.0, 10.0, 100.0, 100.0)), + style: Some(deckcraft_model::ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent1)), ..Default::default() }; - sh.effects = Some(slidecraft_model::Effects { soft_edge: Some(5.0), ..Default::default() }); + sh.effects = Some(deckcraft_model::Effects { soft_edge: Some(5.0), ..Default::default() }); sl.shapes.push(sh); assert!(needs_filters(&s, &s.slides[0])); let img = RENDERER.with(|r| r.borrow_mut().slide(&s, &s.slides[0], 0, &RenderOpts { scale: 0.5, threads: 4, ..Default::default() })); diff --git a/crates/render/src/paint.rs b/crates/render/src/paint.rs index 3c79462..e377042 100644 --- a/crates/render/src/paint.rs +++ b/crates/render/src/paint.rs @@ -1,11 +1,11 @@ //! Fills: solid, gradient, picture and pattern paints. +use deckcraft_color::Rgba; +use deckcraft_geom::preset::FillMode; +use deckcraft_model::resolve::Ctx; +use deckcraft_model::style::{Fill, GradientShape, PictureFill, PictureMode}; +use deckcraft_model::{PhType, Presentation}; use kurbo::{Affine, BezPath, Point, Rect, Shape as _}; -use slidecraft_color::Rgba; -use slidecraft_geom::preset::FillMode; -use slidecraft_model::resolve::Ctx; -use slidecraft_model::style::{Fill, GradientShape, PictureFill, PictureMode}; -use slidecraft_model::{PhType, Presentation}; use vello_cpu::{RenderContext, peniko}; use crate::{color, images, shade}; diff --git a/crates/render/src/placed.rs b/crates/render/src/placed.rs index 9d38555..241897d 100644 --- a/crates/render/src/placed.rs +++ b/crates/render/src/placed.rs @@ -1,11 +1,11 @@ //! Laid-out text and click areas of a slide, in slide coordinates, without drawing — for exports //! that put real text over a raster (PDF text layer, links) and for text extraction. +use deckcraft_model::resolve::{self, Ctx}; +use deckcraft_model::text::{Hyperlink, TextBody}; +use deckcraft_model::{Presentation, Shape, ShapeKind}; +use deckcraft_text::{Opts, TextLayout}; use kurbo::{Affine, Rect}; -use slidecraft_model::resolve::{self, Ctx}; -use slidecraft_model::text::{Hyperlink, TextBody}; -use slidecraft_model::{Presentation, Shape, ShapeKind}; -use slidecraft_text::{Opts, TextLayout}; use crate::{SlideFields, shape_geometry}; @@ -71,7 +71,7 @@ fn place(out: &mut Placed, rctx: &Ctx, fields: &SlideFields, s: &Shape, parent: match &s.kind { ShapeKind::Group { children, child } => { let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h); - let gm = parent * slidecraft_geom::group_child_affine(&x, ch); + let gm = parent * deckcraft_geom::group_child_affine(&x, ch); for c in children { place(out, rctx, fields, c, gm, depth + 1); } @@ -80,7 +80,7 @@ fn place(out: &mut Placed, rctx: &Ctx, fields: &SlideFields, s: &Shape, parent: _ => { let Some(body) = s.text.as_ref().filter(|b| !b.is_empty()) else { return }; let tr = shape_geometry(s, x.w, x.h).text_rect; - let layout = slidecraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: None, no_shrink: false }); + let layout = deckcraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: None, no_shrink: false }); let m = if layout.rotation != 0.0 { let c = tr.center().to_vec2(); m * Affine::translate(c) * Affine::rotate(layout.rotation.to_radians()) * Affine::translate(-c) diff --git a/crates/render/src/table.rs b/crates/render/src/table.rs index 6a5c188..15851d9 100644 --- a/crates/render/src/table.rs +++ b/crates/render/src/table.rs @@ -1,12 +1,12 @@ //! Tables: cell fills from the table style (or overrides), borders, and cell text. +use deckcraft_color::{ColorTransform, Rgba, SchemeSlot}; +use deckcraft_model::resolve::Ctx; +use deckcraft_model::style::{ColorRef, Fill}; +use deckcraft_model::table::Table; +use deckcraft_model::text::Anchor; +use deckcraft_model::{Presentation, Shape}; use kurbo::{Affine, BezPath, Rect, Shape as _}; -use slidecraft_color::{ColorTransform, Rgba, SchemeSlot}; -use slidecraft_model::resolve::Ctx; -use slidecraft_model::style::{ColorRef, Fill}; -use slidecraft_model::table::Table; -use slidecraft_model::text::Anchor; -use slidecraft_model::{Presentation, Shape}; use vello_cpu::{RenderContext, peniko}; use crate::color; @@ -42,7 +42,7 @@ pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook { let body_r = if t.first_row { r.wrapping_sub(1) } else { r }; let band = t.band_row && !header && !total && body_r % 2 == 0; let band_c = t.band_col && c.is_multiple_of(2); - let tint = |k: i32| slidecraft_color::apply(a, &[ColorTransform::Tint(k)]); + let tint = |k: i32| deckcraft_color::apply(a, &[ColorTransform::Tint(k)]); let white = Rgba::WHITE; let dark_text = ctx.color(&ColorRef::scheme(SchemeSlot::Tx1), None); match kind { @@ -64,9 +64,9 @@ pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook { fill: Some(if header { Rgba::BLACK } else if band || band_c { - slidecraft_color::apply(a, &[ColorTransform::Shade(60000)]) + deckcraft_color::apply(a, &[ColorTransform::Shade(60000)]) } else { - slidecraft_color::apply(a, &[ColorTransform::Shade(75000)]) + deckcraft_color::apply(a, &[ColorTransform::Shade(75000)]) }), text: Some(white), bold: header || total || first_col, @@ -123,7 +123,7 @@ pub fn cell_rects(t: &Table) -> Vec<(usize, usize, Rect)> { out } -pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, m: Affine, _w: f64, _h: f64, fields: &dyn slidecraft_text::Fields) { +pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, m: Affine, _w: f64, _h: f64, fields: &dyn deckcraft_text::Fields) { let _ = pres; let rects = cell_rects(t); for (r, c, rect) in &rects { @@ -132,7 +132,7 @@ pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, ctx.set_transform(m); match &cell.fill { Some(f) => { - crate::paint::fill_path(ctx, rctx, f, None, &rect.to_path(0.1), *rect, m, slidecraft_geom::preset::FillMode::Norm, 1.0, rctx.pres) + crate::paint::fill_path(ctx, rctx, f, None, &rect.to_path(0.1), *rect, m, deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres) } None => { if let Some(fc) = lk.fill { diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 2ec5d06..2a2074d 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1,8 +1,8 @@ use super::*; -use slidecraft_model::chart::ChartType; -use slidecraft_model::style::ColorRef; -use slidecraft_model::text::TextBody; -use slidecraft_model::{Chart, Geom, Presentation, Shape, ShapeId, ShapeKind, ShapeStyle, Table, Xfrm}; +use deckcraft_model::chart::ChartType; +use deckcraft_model::style::ColorRef; +use deckcraft_model::text::TextBody; +use deckcraft_model::{Chart, Geom, Presentation, Shape, ShapeId, ShapeKind, ShapeStyle, Table, Xfrm}; fn deck_with(shapes: Vec) -> Presentation { let mut p = Presentation::default(); @@ -24,12 +24,12 @@ fn shape_fill_uses_theme_accent() { let sh = Shape { id: ShapeId(500), xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), - style: Some(ShapeStyle::accent(slidecraft_color::SchemeSlot::Accent1)), + style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent1)), ..Default::default() }; let p = deck_with(vec![sh]); let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }); - let accent = p.masters[0].theme.colors.get(slidecraft_color::SchemeSlot::Accent1); + let accent = p.masters[0].theme.colors.get(deckcraft_color::SchemeSlot::Accent1); let px = img.pixel(200, 150); assert!((px[0] as i32 - accent.r as i32).abs() <= 2 && (px[2] as i32 - accent.b as i32).abs() <= 2, "{px:?} vs {accent:?}"); assert_eq!(img.pixel(50, 50), [255, 255, 255, 255]); @@ -49,14 +49,14 @@ fn text_draws_ink() { #[test] fn every_preset_renders_without_panic() { - let shapes: Vec = slidecraft_geom::preset::CATALOG + let shapes: Vec = deckcraft_geom::preset::CATALOG .iter() .enumerate() .map(|(i, pr)| Shape { id: ShapeId(1000 + i as u32), xfrm: Some(Xfrm::new((i % 16) as f64 * 60.0, (i / 16) as f64 * 55.0, 50.0, 45.0)), geom: Geom::preset(pr.name), - style: Some(ShapeStyle::accent(slidecraft_color::SchemeSlot::Accent2)), + style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent2)), ..Default::default() }) .collect(); @@ -70,12 +70,12 @@ fn effects_tables_charts_render() { let mut sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(50.0, 50.0, 200.0, 100.0)), - style: Some(ShapeStyle::accent(slidecraft_color::SchemeSlot::Accent1)), + style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent1)), ..Default::default() }; - sh.effects = Some(slidecraft_model::Effects { - outer_shadow: Some(slidecraft_model::style::Shadow { - color: ColorRef::rgb(slidecraft_color::Rgba::BLACK), + sh.effects = Some(deckcraft_model::Effects { + outer_shadow: Some(deckcraft_model::style::Shadow { + color: ColorRef::rgb(deckcraft_color::Rgba::BLACK), blur: 8.0, dist: 6.0, dir: 45.0, @@ -87,7 +87,7 @@ fn effects_tables_charts_render() { align: String::new(), rotate_with_shape: false, }), - glow: Some(slidecraft_model::style::Glow { color: ColorRef::rgb(slidecraft_color::Rgba::rgb(255, 200, 0)), radius: 6.0 }), + glow: Some(deckcraft_model::style::Glow { color: ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 200, 0)), radius: 6.0 }), soft_edge: Some(4.0), ..Default::default() }); diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index f8adf47..d4bcb49 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "slidecraft-text" +name = "deckcraft-text" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft text layout: runs to shaped glyph lines with bullets, indents, spacing, alignment, anchoring, autofit and caret hit testing" +description = "DeckCraft text layout: runs to shaped glyph lines with bullets, indents, spacing, alignment, anchoring, autofit and caret hit testing" [lints] workspace = true [dependencies] -slidecraft-model = { workspace = true } -slidecraft-geom = { workspace = true } -slidecraft-color = { workspace = true } -slidecraft-fonts = { workspace = true } +deckcraft-model = { workspace = true } +deckcraft-geom = { workspace = true } +deckcraft-color = { workspace = true } +deckcraft-fonts = { workspace = true } unicode-linebreak = { workspace = true } unicode-segmentation = { workspace = true } diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index aadc053..5f49f2f 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -10,12 +10,12 @@ use std::sync::Arc; -use slidecraft_color::Rgba; -use slidecraft_fonts::{FontDb, FontFace}; -use slidecraft_geom::{Point, Rect}; -use slidecraft_model::resolve::{self, Ctx}; -use slidecraft_model::text::{Align, Anchor, AutoFit, BodyProps, Bullet, Caps, ParaProps, RunKind, RunProps, Spacing, Strike, TextBody, TextDir}; -use slidecraft_model::{Fill, Shape}; +use deckcraft_color::Rgba; +use deckcraft_fonts::{FontDb, FontFace}; +use deckcraft_geom::{Point, Rect}; +use deckcraft_model::resolve::{self, Ctx}; +use deckcraft_model::text::{Align, Anchor, AutoFit, BodyProps, Bullet, Caps, ParaProps, RunKind, RunProps, Spacing, Strike, TextBody, TextDir}; +use deckcraft_model::{Fill, Shape}; /// A run of glyphs in one face, size and colour. #[derive(Clone)] @@ -37,7 +37,7 @@ pub struct GlyphRun { pub link: bool, /// Alpha of the fill (0–1). pub alpha: f64, - pub gradient: Option, + pub gradient: Option, } impl std::fmt::Debug for GlyphRun { @@ -130,7 +130,7 @@ struct Style { highlight: Option, outline: Option<(Rgba, f64)>, link: bool, - gradient: Option, + gradient: Option, } /// One shaped character cell of a paragraph. @@ -357,7 +357,7 @@ fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, let fi = face_index(&mut faces, &face); let upem = face.upem.max(1.0); let sz = |ch: char| if small && ch.is_lowercase() { stl.size * 0.8 } else { stl.size }; - let shaped = slidecraft_fonts::shape(&face, &seg, &[], |ch| if upper { ch.to_uppercase().next().unwrap_or(ch) } else { ch }); + let shaped = deckcraft_fonts::shape(&face, &seg, &[], |ch| if upper { ch.to_uppercase().next().unwrap_or(ch) } else { ch }); // byte offset → char index within seg let mut byte_to_char = vec![0usize; seg.len() + 1]; for (ci, (b, _)) in seg.char_indices().enumerate() { @@ -461,7 +461,7 @@ fn map_symbol_bullet(c: &str, font: Option<&str>) -> String { } fn measure_str(face: &FontFace, size: f64, s: &str) -> (f64, Vec<(u32, f64)>) { - let g = slidecraft_fonts::shape(face, s, &[], |c| c); + let g = deckcraft_fonts::shape(face, s, &[], |c| c); let k = size / face.upem.max(1.0); let mut x = 0.0; let mut out = vec![]; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 9065516..7283b70 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1,7 +1,7 @@ use super::*; -use slidecraft_model::defaults; -use slidecraft_model::text::{Paragraph, Run}; -use slidecraft_model::{LayoutType, Presentation, Xfrm}; +use deckcraft_model::defaults; +use deckcraft_model::text::{Paragraph, Run}; +use deckcraft_model::{LayoutType, Presentation, Xfrm}; fn setup(text: &str, w: f64, h: f64) -> (Presentation, Shape) { let mut p = Presentation::default(); @@ -89,7 +89,7 @@ fn empty_and_hostile_bodies_never_panic() { t.paragraphs = vec![Paragraph { runs: vec![ Run::new("\t\u{b}\u{b}🙂 ﷽ 日本語"), - Run { text: String::new(), props: Default::default(), kind: slidecraft_model::text::RunKind::Break }, + Run { text: String::new(), props: Default::default(), kind: deckcraft_model::text::RunKind::Break }, ], ..Default::default() }]; diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 16702be..f8f087e 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -1,23 +1,23 @@ [package] -name = "slidecraft-ui-egui" +name = "deckcraft-ui-egui" version.workspace = true edition.workspace = true license.workspace = true -description = "SlideCraft egui frontend (PowerPoint-style UI); swappable — nothing below depends on it" +description = "DeckCraft egui frontend (PowerPoint-style UI); swappable — nothing below depends on it" [lints] workspace = true [dependencies] -slidecraft-geom = { workspace = true } -slidecraft-color = { workspace = true } -slidecraft-model = { workspace = true } -slidecraft-fonts = { workspace = true } -slidecraft-text = { workspace = true } -slidecraft-render = { workspace = true } -slidecraft-anim = { workspace = true } -slidecraft-engine = { workspace = true } -slidecraft-media = { workspace = true } +deckcraft-geom = { workspace = true } +deckcraft-color = { workspace = true } +deckcraft-model = { workspace = true } +deckcraft-fonts = { workspace = true } +deckcraft-text = { workspace = true } +deckcraft-render = { workspace = true } +deckcraft-anim = { workspace = true } +deckcraft-engine = { workspace = true } +deckcraft-media = { workspace = true } egui = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/ui-egui/examples/icon_gallery.rs b/crates/ui-egui/examples/icon_gallery.rs index aafc5f2..6b1a38a 100644 --- a/crates/ui-egui/examples/icon_gallery.rs +++ b/crates/ui-egui/examples/icon_gallery.rs @@ -1,14 +1,14 @@ -//! Renders every SlideCraft icon into a labelled PNG grid, on a light and a dark background. +//! Renders every DeckCraft icon into a labelled PNG grid, on a light and a dark background. //! //! ```sh -//! cargo run -p slidecraft-ui-egui --example icon_gallery -- /tmp/gallery.png +//! cargo run -p deckcraft-ui-egui --example icon_gallery -- /tmp/gallery.png //! ``` //! //! Each cell shows the icon at 32 pt, at 16 pt, and disabled at 16 pt, with its name underneath. //! An optional second argument filters icons by name substring. +use deckcraft_ui_egui::icons::{self, Icon}; use egui::{Align2, Color32, FontId, Rect, pos2, vec2}; -use slidecraft_ui_egui::icons::{self, Icon}; const COLS: usize = 12; const CELL_W: f32 = 110.0; diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index e3301c3..641ae86 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2,11 +2,11 @@ //! and the overlays drawn on top (selection box and handles, smart guides, marquee, drawing //! previews, the text caret and text selection). +use deckcraft_engine::tools::{Hit, box_handles}; +use deckcraft_engine::{Mods, PointerEvent, PointerKind, Target, ToolKind}; +use deckcraft_geom::{Point, Xfrm}; use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, Ui, pos2, vec2}; use serde_json::json; -use slidecraft_engine::tools::{Hit, box_handles}; -use slidecraft_engine::{Mods, PointerEvent, PointerKind, Target, ToolKind}; -use slidecraft_geom::{Point, Xfrm}; use crate::SlideApp; use crate::theme::{self, Tokens}; @@ -201,16 +201,16 @@ fn media_bar(app: &mut SlideApp, ui: &mut Ui, xf: Xf) { fn master_tex( ctx: &egui::Context, - doc: &slidecraft_model::Presentation, + doc: &deckcraft_model::Presentation, master: usize, layout: Option, px: (u32, u32), ) -> Option { - let img = slidecraft_render::render_layout( + let img = deckcraft_render::render_layout( doc, master, layout, - &slidecraft_render::RenderOpts { scale: px.0 as f64 / doc.slide_size.width.max(1.0), edit: true, size: Some(px), ..Default::default() }, + &deckcraft_render::RenderOpts { scale: px.0 as f64 / doc.slide_size.width.max(1.0), edit: true, size: Some(px), ..Default::default() }, ); Some(ctx.load_texture("master-canvas", crate::textures::to_color_image(&img, false), egui::TextureOptions::LINEAR)) } @@ -327,13 +327,13 @@ fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &To let editing = st.selection.text.clone(); for id in &st.selection.shapes { let Some(sh) = st.shape(*id) else { continue }; - let x = slidecraft_engine::cmd::xfrm_of(&st.doc, &st.selection, sh); + let x = deckcraft_engine::cmd::xfrm_of(&st.doc, &st.selection, sh); let a = x.affine(); let corners = [Point::new(0.0, 0.0), Point::new(x.w, 0.0), Point::new(x.w, x.h), Point::new(0.0, x.h)].map(|q| xf.to_screen(a * q)); if sh.is_line() { let (p0, p1) = (xf.to_screen(a * Point::new(0.0, 0.0)), xf.to_screen(a * Point::new(x.w, x.h))); let glued = match sh.kind { - slidecraft_model::ShapeKind::Connector { start, end } => [start.is_some(), end.is_some()], + deckcraft_model::ShapeKind::Connector { start, end } => [start.is_some(), end.is_some()], _ => [false, false], }; for (q, g) in [p0, p1].into_iter().zip(glued) { @@ -364,7 +364,7 @@ fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &To for h in hs { painter.circle(xf.to_screen(h), 4.5, t.handle_fill, Stroke::new(1.0, t.handle_stroke)); } - let geo = slidecraft_render::shape_geometry(sh, x.w, x.h); + let geo = deckcraft_render::shape_geometry(sh, x.w, x.h); for hd in &geo.handles { let q = xf.to_screen(a * hd.pos); painter.circle(q, 4.5, Color32::from_rgb(0xFF, 0xD2, 0x3F), Stroke::new(1.0, Color32::from_rgb(0x8A, 0x6A, 0x00))); @@ -376,26 +376,26 @@ fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &To && !ts.notes && ts.cell.is_none() && let Some(sh) = st.shape(ts.shape) - && let Some(l) = slidecraft_engine::cmd::text::layout_for(st, ts) + && let Some(l) = deckcraft_engine::cmd::text::layout_for(st, ts) { - let x = slidecraft_engine::cmd::xfrm_of(&st.doc, &st.selection, sh); + let x = deckcraft_engine::cmd::xfrm_of(&st.doc, &st.selection, sh); let a = x.affine(); let rot = if l.rotation != 0.0 { let c = l.inner.center().to_vec2(); - slidecraft_geom::Affine::translate(c) * slidecraft_geom::Affine::rotate(l.rotation.to_radians()) * slidecraft_geom::Affine::translate(-c) + deckcraft_geom::Affine::translate(c) * deckcraft_geom::Affine::rotate(l.rotation.to_radians()) * deckcraft_geom::Affine::translate(-c) } else { - slidecraft_geom::Affine::IDENTITY + deckcraft_geom::Affine::IDENTITY }; let m = a * rot; let (s0, s1) = ts.ordered(); - for r in l.selection_rects(slidecraft_text::Pos { para: s0.0, ch: s0.1 }, slidecraft_text::Pos { para: s1.0, ch: s1.1 }) { + for r in l.selection_rects(deckcraft_text::Pos { para: s0.0, ch: s0.1 }, deckcraft_text::Pos { para: s1.0, ch: s1.1 }) { let pts = [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)].map(|q| xf.to_screen(m * q)); painter.add(egui::Shape::convex_polygon(pts.to_vec(), t.text_selection, Stroke::NONE)); } if !ts.is_range() { let blink = (ui.input(|i| i.time) * 1.6).fract() < 0.6; ui.ctx().request_repaint_after(std::time::Duration::from_millis(300)); - if blink && let Some((cx, top, bot)) = l.caret(slidecraft_text::Pos { para: ts.caret.0, ch: ts.caret.1 }) { + if blink && let Some((cx, top, bot)) = l.caret(deckcraft_text::Pos { para: ts.caret.0, ch: ts.caret.1 }) { let (p0, p1) = (xf.to_screen(m * Point::new(cx, top)), xf.to_screen(m * Point::new(cx, bot))); painter.line_segment([p0, p1], Stroke::new(1.5, t.caret)); } @@ -404,9 +404,9 @@ fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &To // Table cell editing: outline the cell. if let Some(ts) = &editing && let (Some((r, c)), Some(sh)) = (ts.cell, st.shape(ts.shape)) - && let slidecraft_model::ShapeKind::Table(tb) = &sh.kind + && let deckcraft_model::ShapeKind::Table(tb) = &sh.kind { - let x = slidecraft_engine::cmd::xfrm_of(&st.doc, &st.selection, sh); + let x = deckcraft_engine::cmd::xfrm_of(&st.doc, &st.selection, sh); let y0: f64 = tb.rows.iter().take(r).map(|rr| rr.height).sum(); let x0: f64 = tb.cols.iter().take(c).sum(); let (w, h) = (tb.cols.get(c).copied().unwrap_or(0.0), tb.rows.get(r).map(|rr| rr.height).unwrap_or(0.0)); @@ -437,11 +437,11 @@ fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &To let path = &app.session.tool.path_preview; if path.len() > 1 { let pts: Vec = if app.session.tool.path_smooth && path.len() > 2 { - let d = slidecraft_engine::cmd::shape::freeform_path(path, false, true); + let d = deckcraft_engine::cmd::shape::freeform_path(path, false, true); let mut v = vec![]; - let bp = slidecraft_render::parse_path(&d); - slidecraft_geom::preset_flatten(&bp, &mut |el| match el { - slidecraft_geom::PathEl::MoveTo(q) | slidecraft_geom::PathEl::LineTo(q) => v.push(q), + let bp = deckcraft_render::parse_path(&d); + deckcraft_geom::preset_flatten(&bp, &mut |el| match el { + deckcraft_geom::PathEl::MoveTo(q) | deckcraft_geom::PathEl::LineTo(q) => v.push(q), _ => {} }); v @@ -493,7 +493,7 @@ fn draw_preview(app: &SlideApp, p: &egui::Painter, xf: Xf, x: Xfrm, t: &Tokens) ToolKind::Shape { preset } => preset.clone(), _ => "rect".into(), }; - if slidecraft_geom::preset::is_line_like(&preset) { + if deckcraft_geom::preset::is_line_like(&preset) { let a = x.affine(); p.line_segment([xf.to_screen(a * Point::new(0.0, 0.0)), xf.to_screen(a * Point::new(x.w, x.h))], Stroke::new(1.5, t.accent)); return; diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 62bf249..060f58e 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -17,8 +17,8 @@ use std::sync::mpsc::Sender; +use deckcraft_engine::{Mods, PointerEvent}; use serde_json::{Value, json}; -use slidecraft_engine::{Mods, PointerEvent}; use crate::SlideApp; @@ -118,7 +118,7 @@ pub fn handle(app: &mut SlideApp, ctx: &egui::Context, req: &ControlRequest) -> let mut last = Value::Null; for e in events { let kind = e.get("kind").and_then(Value::as_str).unwrap_or(""); - let Some(kind) = slidecraft_mcp_kind(kind) else { return err(format!("unknown pointer kind `{kind}`")) }; + let Some(kind) = deckcraft_mcp_kind(kind) else { return err(format!("unknown pointer kind `{kind}`")) }; let (Some(x), Some(y)) = (e.get("x").and_then(Value::as_f64), e.get("y").and_then(Value::as_f64)) else { return err("pointer events need x and y"); }; @@ -242,7 +242,7 @@ pub fn handle(app: &mut SlideApp, ctx: &egui::Context, req: &ControlRequest) -> if i >= d.doc.slides.len() { return err(format!("no slide {i}")); } - let (png, w, h) = slidecraft_engine::cmd::file::render_png( + let (png, w, h) = deckcraft_engine::cmd::file::render_png( &d.doc, i, p.get("scale").and_then(Value::as_f64).unwrap_or(1.0), @@ -254,7 +254,7 @@ pub fn handle(app: &mut SlideApp, ctx: &egui::Context, req: &ControlRequest) -> Some(Err(e)) => err(e), None => err("no writer"), }, - None => ok(json!({"png": slidecraft_engine::cmd::base64_encode(&png), "width": w, "height": h, "slide": i})), + None => ok(json!({"png": deckcraft_engine::cmd::base64_encode(&png), "width": w, "height": h, "slide": i})), } } "ui.resize" => { @@ -307,8 +307,8 @@ fn mods_from(p: &Value) -> Mods { Mods { shift: b("shift"), alt: b("alt"), cmd: b("cmd") || b("ctrl") } } -fn slidecraft_mcp_kind(k: &str) -> Option { - use slidecraft_engine::PointerKind::*; +fn deckcraft_mcp_kind(k: &str) -> Option { + use deckcraft_engine::PointerKind::*; Some(match k { "down" => Down, "drag" => Drag, @@ -320,17 +320,17 @@ fn slidecraft_mcp_kind(k: &str) -> Option { }) } -pub fn tool_kind(name: &str, preset: Option<&str>) -> Option { - use slidecraft_engine::ToolKind; +pub fn tool_kind(name: &str, preset: Option<&str>) -> Option { + use deckcraft_engine::ToolKind; Some(match name { "select" | "selection" => ToolKind::Select, "textBox" | "text" => ToolKind::TextBox, "shape" => ToolKind::Shape { preset: preset.unwrap_or("rect").into() }, - "pen" => ToolKind::Ink { mode: "pen".into(), color: slidecraft_color::Rgba::BLACK, width: 2.0 }, - "highlighter" => ToolKind::Ink { mode: "highlighter".into(), color: slidecraft_color::Rgba::rgb(255, 230, 0), width: 10.0 }, - "eraser" => ToolKind::Ink { mode: "eraser".into(), color: slidecraft_color::Rgba::BLACK, width: 2.0 }, - other if slidecraft_geom::preset::info(other).is_some() => ToolKind::Shape { preset: other.into() }, - other if slidecraft_engine::tools::is_freeform_tool(other) => ToolKind::Shape { preset: other.into() }, + "pen" => ToolKind::Ink { mode: "pen".into(), color: deckcraft_color::Rgba::BLACK, width: 2.0 }, + "highlighter" => ToolKind::Ink { mode: "highlighter".into(), color: deckcraft_color::Rgba::rgb(255, 230, 0), width: 10.0 }, + "eraser" => ToolKind::Ink { mode: "eraser".into(), color: deckcraft_color::Rgba::BLACK, width: 2.0 }, + other if deckcraft_geom::preset::info(other).is_some() => ToolKind::Shape { preset: other.into() }, + other if deckcraft_engine::tools::is_freeform_tool(other) => ToolKind::Shape { preset: other.into() }, _ => return None, }) } @@ -338,10 +338,10 @@ pub fn tool_kind(name: &str, preset: Option<&str>) -> Option) -> Value { let [w, h] = image.size; let rgba: Vec = image.pixels.iter().flat_map(|c| c.to_array()).collect(); - let img = slidecraft_render::Image { width: w as u32, height: h as u32, pixels: rgba }; + let img = deckcraft_render::Image { width: w as u32, height: h as u32, pixels: rgba }; let png = img.to_png(); let Some(path) = path else { - return json!({"ok": true, "result": {"width": w, "height": h, "png": slidecraft_engine::cmd::base64_encode(&png)}}); + return json!({"ok": true, "result": {"width": w, "height": h, "png": deckcraft_engine::cmd::base64_encode(&png)}}); }; match app.services.write.as_mut() { Some(wr) => match wr(path, &png) { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index f799009..6beaaa1 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -43,7 +43,7 @@ fn title(id: &str) -> &'static str { "renameShape" => "Rename", "renameLayout" => "Rename Layout", "paragraph" => "Paragraph", - "about" => "About SlideCraft", + "about" => "About DeckCraft", "preferences" => "Preferences", "export" => "Export", "symbol" => "Symbol", @@ -55,8 +55,8 @@ fn title(id: &str) -> &'static str { "chartData" => "Chart Data", "trim" => "Trim Media", "smartart" => "Choose a SmartArt Graphic", - "quit" => "SlideCraft", - _ => "SlideCraft", + "quit" => "DeckCraft", + _ => "DeckCraft", } } @@ -134,11 +134,11 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok || cancel } "slideSize" => { - let cur = app.session.active().map(|s| s.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE); + let cur = app.session.active().map(|s| s.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE); ui.label("Slides sized for:"); let preset = d.get("preset", ""); egui::ComboBox::from_id_salt("sizes").selected_text(if preset.is_empty() { "Custom" } else { &preset }).show_ui(ui, |ui| { - for (label, w, h) in slidecraft_model::defaults::SLIDE_SIZES { + for (label, w, h) in deckcraft_model::defaults::SLIDE_SIZES { if ui.selectable_label(preset == *label, *label).clicked() { d.fields.insert("preset".into(), label.to_string()); d.fields.insert("w".into(), format!("{:.2}", w / 72.0)); @@ -457,15 +457,15 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } else if let (Some(dl), Some(s)) = (app.services.download.as_mut(), app.session.active()) { let bytes = match ext { "png" | "jpg" => { - let img = slidecraft_render::render_slide( + let img = deckcraft_render::render_slide( &s.doc, s.selection.slide, - &slidecraft_render::RenderOpts { scale: 2.0, ..Default::default() }, + &deckcraft_render::RenderOpts { scale: 2.0, ..Default::default() }, ); if ext == "png" { img.to_png() } else { img.to_jpeg(92) } } - "pdf" => slidecraft_pdf_bytes(&s.doc, &pdf_params), - other => slidecraft_engine::cmd::file::save_bytes(&s.doc, other).unwrap_or_default(), + "pdf" => deckcraft_pdf_bytes(&s.doc, &pdf_params), + other => deckcraft_engine::cmd::file::save_bytes(&s.doc, other).unwrap_or_default(), }; dl(&suggested, &bytes); } @@ -506,7 +506,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let (ok, cancel) = buttons(ui, "Insert"); if ok && !v.is_empty() { let pretty = pretty_equation(&v); - let size = app.session.active().map(|s| s.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE); + let size = app.session.active().map(|s| s.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE); run(app, "insert.textBox", json!({"rect": [size.width / 2.0 - 150.0, size.height / 2.0 - 25.0, 300, 50], "text": pretty})); run(app, "text.exit", json!({})); run(app, "format.font", json!({"family": "Liberation Serif"})); @@ -603,7 +603,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let ch = app.session.active().and_then(|s| { s.selected_shapes() .into_iter() - .find_map(|x| if let slidecraft_model::ShapeKind::Chart(c) = &x.kind { Some(((**c).clone(), x.id)) } else { None }) + .find_map(|x| if let deckcraft_model::ShapeKind::Chart(c) = &x.kind { Some(((**c).clone(), x.id)) } else { None }) }); let Some((c, id)) = ch else { ui.label("Select a chart first."); @@ -774,27 +774,27 @@ pub fn about(_app: &mut SlideApp, ui: &mut Ui) { ui.vertical_centered(|ui| { let (r, _) = ui.allocate_exact_size(vec2(64.0, 64.0), Sense::hover()); paint_logo(ui.painter(), r); - ui.label(egui::RichText::new("SlideCraft").font(theme::bold(22.0))); + ui.label(egui::RichText::new("DeckCraft").font(theme::bold(22.0))); ui.label(egui::RichText::new(version_line()).color(t.text_dim)); ui.add_space(6.0); ui.label("Presentations and slide shows, rebuilt from scratch in pure Rust."); ui.label("An open-source, clean-room project. Not affiliated with Microsoft."); ui.add_space(6.0); - ui.hyperlink_to("Join our community on Discord", slidecraft_engine::links::DISCORD); - ui.hyperlink_to("getartcraft.com/apps/slidecraft", slidecraft_engine::links::APP_PAGE); - ui.hyperlink_to("Source on GitHub", slidecraft_engine::links::GITHUB); + ui.hyperlink_to("Join our community on Discord", deckcraft_engine::links::DISCORD); + ui.hyperlink_to("getartcraft.com/apps/deckcraft", deckcraft_engine::links::APP_PAGE); + ui.hyperlink_to("Source on GitHub", deckcraft_engine::links::GITHUB); ui.add_space(6.0); - ui.label(egui::RichText::new("MIT OR Apache-2.0 · © 2026 ArtCraft Team and the SlideCraft contributors").size(11.0).color(t.text_faint)); + ui.label(egui::RichText::new("MIT OR Apache-2.0 · © 2026 ArtCraft Team and the DeckCraft contributors").size(11.0).color(t.text_faint)); }); } /// "Version X.Y.Z" plus, for release builds, the short commit and build date. The version comes /// from `[workspace.package] version` (the single source of truth, `cargo xtask version`); the -/// release workflow sets `SLIDECRAFT_BUILD_SHA` / `SLIDECRAFT_BUILD_DATE` at build time. +/// release workflow sets `DECKCRAFT_BUILD_SHA` / `DECKCRAFT_BUILD_DATE` at build time. pub fn version_line() -> String { let mut s = format!("Version {}", env!("CARGO_PKG_VERSION")); - let sha = option_env!("SLIDECRAFT_BUILD_SHA").map(|s| s.get(..9).unwrap_or(s)).filter(|s| !s.is_empty()); - let date = option_env!("SLIDECRAFT_BUILD_DATE").filter(|s| !s.is_empty()); + let sha = option_env!("DECKCRAFT_BUILD_SHA").map(|s| s.get(..9).unwrap_or(s)).filter(|s| !s.is_empty()); + let date = option_env!("DECKCRAFT_BUILD_DATE").filter(|s| !s.is_empty()); match (sha, date) { (Some(sha), Some(date)) => s.push_str(&format!(" ({sha}, {date})")), (Some(x), None) | (None, Some(x)) => s.push_str(&format!(" ({x})")), @@ -803,7 +803,7 @@ pub fn version_line() -> String { s } -/// SlideCraft's mark, drawn in code: a slide card with a play triangle in our orange. +/// DeckCraft's mark, drawn in code: a slide card with a play triangle in our orange. pub fn paint_logo(p: &egui::Painter, r: Rect) { let orange = Color32::from_rgb(0xF2, 0x6B, 0x1D); let deep = Color32::from_rgb(0xC2, 0x4E, 0x14); @@ -828,7 +828,7 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(40.0); let (r, _) = ui.allocate_exact_size(vec2(40.0, 40.0), Sense::hover()); paint_logo(ui.painter(), r); - ui.label(egui::RichText::new("SlideCraft").font(theme::bold(26.0))); + ui.label(egui::RichText::new("DeckCraft").font(theme::bold(26.0))); }); ui.add_space(16.0); ui.horizontal(|ui| { @@ -838,7 +838,7 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(8.0); ui.horizontal_wrapped(|ui| { ui.add_space(40.0); - for th in slidecraft_model::theme::builtin_themes() { + for th in deckcraft_model::theme::builtin_themes() { let (r, resp) = ui.allocate_exact_size(vec2(180.0, 128.0), Sense::click()); let tile = Rect::from_min_size(r.min, vec2(180.0, 101.0)); ribbon::paint_theme_tile(ui.painter(), tile, &th); @@ -858,7 +858,7 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { let _ = app.run("app.openDialog", json!({})); } if ui.button("Open the sample deck").clicked() - && let Err(e) = slidecraft_engine::sample::open_sample(&mut app.session) + && let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) { app.set_status(e.to_string()); } @@ -883,12 +883,12 @@ pub fn start_screen(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(30.0); ui.horizontal(|ui| { ui.add_space(40.0); - ui.hyperlink_to("Join the ArtCraft community on Discord", slidecraft_engine::links::DISCORD); + ui.hyperlink_to("Join the ArtCraft community on Discord", deckcraft_engine::links::DISCORD); }); }); } /// PDF bytes with the export dialog's options (web: downloaded instead of written). -fn slidecraft_pdf_bytes(doc: &slidecraft_model::Presentation, params: &serde_json::Value) -> Vec { - slidecraft_engine::cmd::file::pdf_bytes(doc, params).unwrap_or_default() +fn deckcraft_pdf_bytes(doc: &deckcraft_model::Presentation, params: &serde_json::Value) -> Vec { + deckcraft_engine::cmd::file::pdf_bytes(doc, params).unwrap_or_default() } diff --git a/crates/ui-egui/src/fillui.rs b/crates/ui-egui/src/fillui.rs index 6e4bd38..a0e60fb 100644 --- a/crates/ui-egui/src/fillui.rs +++ b/crates/ui-egui/src/fillui.rs @@ -1,11 +1,11 @@ //! The gradient editor of the Format pane (shape fill and slide background): preset variations, //! type, direction, angle and a stop bar (add, drag, recolour, set transparency, remove stops). +use deckcraft_color::{ColorScheme, ColorTransform, SchemeSlot}; +use deckcraft_model::ColorRef; +use deckcraft_model::style::{Gradient, GradientShape, GradientStop}; use egui::{Color32, CornerRadius, Rect, Sense, Stroke, Ui, pos2, vec2}; use serde_json::{Value, json}; -use slidecraft_color::{ColorScheme, ColorTransform, SchemeSlot}; -use slidecraft_model::ColorRef; -use slidecraft_model::style::{Gradient, GradientShape, GradientStop}; use crate::ribbon::cref_param; use crate::theme::{self, Tokens}; diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 2ac9bca..ff9897c 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -29,7 +29,7 @@ const DARK: Color32 = Color32::from_rgb(0x2B, 0x2B, 0x2B); macro_rules! icons { ($($(#[$m:meta])* $v:ident => $n:literal,)*) => { - /// Every icon SlideCraft draws. + /// Every icon DeckCraft draws. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Icon { $($(#[$m])* $v,)* diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 5b4f422..83aace2 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1,4 +1,4 @@ -//! SlideCraft's egui frontend: a PowerPoint-style UI over `slidecraft-engine`. +//! DeckCraft's egui frontend: a PowerPoint-style UI over `deckcraft-engine`. //! //! The UI is thin: every action goes through [`SlideApp::run`], which handles UI commands //! (views, panes, zoom) here and sends everything else to the engine. The ribbon, menus, @@ -25,9 +25,9 @@ pub mod widgets; use std::sync::mpsc::{Receiver, Sender}; +use deckcraft_engine::{Mods, Session, UiRequest}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; -use slidecraft_engine::{Mods, Session, UiRequest}; pub use control::{ControlRequest, ControlResponse}; @@ -56,7 +56,7 @@ pub struct Services { /// Image on the system clipboard (PNG bytes), when the host can read one. pub clipboard_image: Option Option>>>, /// Audio output for media playback (cpal on desktop); without one media plays silently. - pub audio_out: Option>, + pub audio_out: Option>, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -149,7 +149,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("show.start", "Slide Show", Some("F5"), "{from?: index}"), ("show.presenter", "Presenter View", None, "{on?: bool}"), ("show.end", "End Show", None, "{}"), - ("app.about", "About SlideCraft", None, "{}"), + ("app.about", "About DeckCraft", None, "{}"), ("app.palette", "Command Palette", Some("Cmd+Shift+P"), "{}"), ("app.preferences", "Preferences…", Some("Cmd+,"), "{}"), ("app.openDialog", "Open…", None, "{purpose?}"), @@ -331,14 +331,14 @@ impl SlideApp { "app.insertPictureDialog" => self.pick_and_insert("picture", "insert.picture"), "app.insertAudioDialog" => self.pick_and_insert("audio", "insert.audio"), "app.insertVideoDialog" => self.pick_and_insert("video", "insert.video"), - "app.saveAsDialog" => self.save_as_dialog("slidecraft"), + "app.saveAsDialog" => self.save_as_dialog("deckcraft"), "app.exportDialog" => self.dialog = Some(dialogs::Dialog::new("export")), "app.dialog" => { let d = p.get("id").and_then(Value::as_str).ok_or("missing `id`")?; self.dialog = Some(dialogs::Dialog::new(d)); } "app.links" => { - use slidecraft_engine::links::*; + use deckcraft_engine::links::*; return Ok(json!({"discord": DISCORD, "website": WEBSITE, "apps": APPS, "appPage": APP_PAGE, "github": GITHUB})); } _ => return Err(format!("unknown UI command `{id}`")), @@ -378,7 +378,7 @@ impl SlideApp { let r = match self.services.read.as_ref().map(|r| r(&path)) { Some(Ok(bytes)) => { let name = std::path::Path::new(&path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(); - self.session.execute(cmd, &json!({"name": name, "data": slidecraft_engine::cmd::base64_encode(&bytes)})) + self.session.execute(cmd, &json!({"name": name, "data": deckcraft_engine::cmd::base64_encode(&bytes)})) } _ => self.session.execute(cmd, &json!({"path": path})), }; @@ -393,7 +393,7 @@ impl SlideApp { pub fn save_as_dialog(&mut self, format: &str) { let name = self.session.active().map(|d| d.title()).unwrap_or_else(|| "Presentation".into()); - let ext = if format == "pptx" { "pptx" } else { "slidecraft" }; + let ext = if format == "pptx" { "pptx" } else { "deckcraft" }; let suggested = format!("{name}.{ext}"); if let Some(pick) = self.services.pick_save.as_mut() { if let Some(path) = pick(&suggested) { @@ -409,7 +409,7 @@ impl SlideApp { } else if let Some(dl) = self.services.download.as_mut() && let Some(d) = self.session.active() { - match slidecraft_engine::cmd::file::save_bytes(&d.doc, ext) { + match deckcraft_engine::cmd::file::save_bytes(&d.doc, ext) { Ok(bytes) => dl(&suggested, &bytes), Err(e) => self.set_status(e.to_string()), } @@ -437,7 +437,7 @@ impl SlideApp { Err(e) => self.set_status(e.to_string()), } } else { - self.save_as_dialog("slidecraft"); + self.save_as_dialog("deckcraft"); } } @@ -455,7 +455,7 @@ impl SlideApp { UiRequest::Message { text } => self.set_status(text), UiRequest::PickFile { purpose } => { if purpose == "saveAs" { - self.save_as_dialog("slidecraft"); + self.save_as_dialog("deckcraft"); } } UiRequest::Media { action, shape, ms } => { @@ -479,9 +479,14 @@ impl SlideApp { /// A dropped or picked file: presentations open, media is inserted. pub fn open_or_insert_bytes(&mut self, name: &str, bytes: &[u8]) { let lower = name.to_ascii_lowercase(); - let data = slidecraft_engine::cmd::base64_encode(bytes); - let ct = slidecraft_engine::cmd::insert::content_type(name, bytes); - let cmd = if lower.ends_with(".slidecraft") || lower.ends_with(".pptx") || lower.ends_with(".potx") || lower.ends_with(".ppsx") { + let data = deckcraft_engine::cmd::base64_encode(bytes); + let ct = deckcraft_engine::cmd::insert::content_type(name, bytes); + let cmd = if lower.ends_with(".deckcraft") + || lower.ends_with(".slidecraft") + || lower.ends_with(".pptx") + || lower.ends_with(".potx") + || lower.ends_with(".ppsx") + { "file.openBytes" } else if ct.starts_with("image/") { "insert.picture" @@ -492,7 +497,7 @@ impl SlideApp { } else if lower.ends_with(".txt") || lower.ends_with(".md") { "slide.fromOutline" } else { - self.set_status(format!("{name}: SlideCraft can't open this kind of file")); + self.set_status(format!("{name}: DeckCraft can't open this kind of file")); return; }; let p = if cmd == "slide.fromOutline" { json!({"text": String::from_utf8_lossy(bytes)}) } else { json!({"name": name, "data": data}) }; @@ -616,7 +621,7 @@ impl SlideApp { let r = match img { Some(png) => self .session - .execute("insert.picture", &json!({"name": "Pasted image.png", "data": slidecraft_engine::cmd::base64_encode(&png)})), + .execute("insert.picture", &json!({"name": "Pasted image.png", "data": deckcraft_engine::cmd::base64_encode(&png)})), None => self.session.execute("edit.paste", &json!({"text": text})), }; if let Err(e) = r { @@ -705,7 +710,7 @@ impl SlideApp { if self.show.is_none() && let Some(d) = self.session.active() { - let here: Vec = if d.selection.target == slidecraft_engine::Target::Slides { + let here: Vec = if d.selection.target == deckcraft_engine::Target::Slides { media::slide_media(&d.doc, d.selection.slide).into_iter().map(|m| m.0).collect() } else { vec![] diff --git a/crates/ui-egui/src/media.rs b/crates/ui-egui/src/media.rs index a470259..a101ce2 100644 --- a/crates/ui-egui/src/media.rs +++ b/crates/ui-egui/src/media.rs @@ -1,13 +1,13 @@ -//! Media playback in the UI host: the [`slidecraft_media::Player`] (audio out + clocks), video +//! Media playback in the UI host: the [`deckcraft_media::Player`] (audio out + clocks), video //! feeds and their textures, the editor's media control bar, and the engine's `media.*` requests. use std::collections::HashMap; use std::sync::Arc; +use deckcraft_engine::{MediaStatus, Session}; +use deckcraft_media::{AudioOut, ClipParams, Frame, PlayState, Player, VideoFeed, VoiceSpec, VoiceStatus}; +use deckcraft_model::{MediaClip, Presentation, ShapeId, ShapeKind}; use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHandle, Ui, pos2, vec2}; -use slidecraft_engine::{MediaStatus, Session}; -use slidecraft_media::{AudioOut, ClipParams, Frame, PlayState, Player, VideoFeed, VoiceSpec, VoiceStatus}; -use slidecraft_model::{MediaClip, Presentation, ShapeId, ShapeKind}; use crate::icons::{self, Icon}; @@ -86,7 +86,7 @@ impl MediaHost { let duration = if clip.duration_ms > 0 { clip.duration_ms as f64 / 1000.0 } else { - *self.durations.entry(media_key).or_insert_with(|| slidecraft_media::probe(&bytes).map(|i| i.duration()).unwrap_or(0.0)) + *self.durations.entry(media_key).or_insert_with(|| deckcraft_media::probe(&bytes).map(|i| i.duration()).unwrap_or(0.0)) }; let spec = VoiceSpec { clip: ClipParams::from_ms(clip.trim_start_ms, clip.trim_end_ms, clip.fade_in_ms, clip.fade_out_ms, clip.volume), @@ -221,7 +221,7 @@ impl MediaHost { /// The clip of media shape `id` anywhere in the deck. pub fn find_clip(doc: &Presentation, id: ShapeId) -> Option { - fn walk(shapes: &[slidecraft_model::Shape], id: ShapeId) -> Option { + fn walk(shapes: &[deckcraft_model::Shape], id: ShapeId) -> Option { shapes.iter().find_map(|s| match &s.kind { ShapeKind::Media(m) if s.id == id => Some(m.clone()), ShapeKind::Group { children, .. } => walk(children, id), @@ -232,7 +232,7 @@ pub fn find_clip(doc: &Presentation, id: ShapeId) -> Option { } /// Media shapes directly on slide `index`: (id, box in slide points, clip). -pub fn slide_media(doc: &Presentation, index: usize) -> Vec<(ShapeId, slidecraft_geom::Xfrm, MediaClip)> { +pub fn slide_media(doc: &Presentation, index: usize) -> Vec<(ShapeId, deckcraft_geom::Xfrm, MediaClip)> { let Some(s) = doc.slides.get(index) else { return vec![] }; s.shapes .iter() diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 91ca5dc..786ad6a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1,8 +1,8 @@ //! UI-level shortcuts, the command palette, and the menu structure (shared with the native macOS //! menu bar in the desktop app). +use deckcraft_engine::Mods; use serde_json::json; -use slidecraft_engine::Mods; use crate::SlideApp; use crate::theme::{self, Tokens}; @@ -240,6 +240,6 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)> { ("Set Up Show…", "show.setup"), ], ), - ("Help", vec![("About SlideCraft", "app.about"), ("Command Palette", "app.palette")]), + ("Help", vec![("About DeckCraft", "app.about"), ("Command Palette", "app.palette")]), ] } diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index f030ce2..6d37463 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -1,9 +1,9 @@ //! Task panes on the right: Format Shape, Format Background, Animation, Selection, Comments and //! Design Ideas. +use deckcraft_model::{AnimClass, AnimStart, Fill, ShapeKind}; use egui::{Align2, Color32, CornerRadius, Rect, Sense, Stroke, Ui, pos2, vec2}; use serde_json::{Value, json}; -use slidecraft_model::{AnimClass, AnimStart, Fill, ShapeKind}; use crate::icons::{self, Icon}; use crate::ribbon::{cref_param, paint_anim_tile}; @@ -59,8 +59,8 @@ fn run(app: &mut SlideApp, id: &str, p: Value) { let _ = app.run(id, p); } -fn scheme(app: &SlideApp) -> slidecraft_color::ColorScheme { - app.session.active().and_then(|d| d.doc.masters.first().map(|m| m.scheme())).unwrap_or_else(|| slidecraft_model::Theme::default().colors) +fn scheme(app: &SlideApp) -> deckcraft_color::ColorScheme { + app.session.active().and_then(|d| d.doc.masters.first().map(|m| m.scheme())).unwrap_or_else(|| deckcraft_model::Theme::default().colors) } fn color_button( @@ -68,7 +68,7 @@ fn color_button( ui: &mut Ui, label: &str, none_label: Option<&str>, - on_pick: impl Fn(Option) -> (String, Value), + on_pick: impl Fn(Option) -> (String, Value), ) { let sc = scheme(app); ui.horizontal(|ui| { @@ -92,7 +92,7 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { ui.label(egui::RichText::new("Select a shape to format it.").color(t.text_dim)); return; }; - let x = slidecraft_engine::cmd::xfrm_of(&st.doc, &st.selection, &sh); + let x = deckcraft_engine::cmd::xfrm_of(&st.doc, &st.selection, &sh); // Tabs as icons. ui.horizontal(|ui| { for (tab, icon, tip) in [ @@ -300,7 +300,7 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) { } let mut rot = g.rotate_with_shape; if ui.checkbox(&mut rot, "Rotate with shape").changed() { - let ng = slidecraft_model::style::Gradient { rotate_with_shape: rot, ..g.clone() }; + let ng = deckcraft_model::style::Gradient { rotate_with_shape: rot, ..g.clone() }; run(app, "shape.fill", json!({"gradient": crate::fillui::gradient_json(&ng)})); } } @@ -343,7 +343,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { let sc = scheme(app); let bg = app.session.active().and_then(|d| d.current_slide().and_then(|s| s.background.clone())); let fill = match &bg { - Some(slidecraft_model::Background::Fill { fill }) => Some(fill.clone()), + Some(deckcraft_model::Background::Fill { fill }) => Some(fill.clone()), _ => None, }; let kind = match &fill { @@ -367,7 +367,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { match k { "solid" => run(app, "design.background", json!({"color": "bg1"})), "gradient" => { - let mut g = crate::fillui::default_gradient(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Bg2), 90.0); + let mut g = crate::fillui::default_gradient(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg2), 90.0); g.rotate_with_shape = false; run(app, "design.background", json!({"gradient": crate::fillui::gradient_json(&g)})); } @@ -376,7 +376,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { && let Some(path) = pick("picture") && let Some(Ok(bytes)) = app.services.read.as_ref().map(|r| r(&path)) { - let _ = app.run("design.background", json!({"picture": slidecraft_engine::cmd::base64_encode(&bytes)})); + let _ = app.run("design.background", json!({"picture": deckcraft_engine::cmd::base64_encode(&bytes)})); } } "pattern" => run(app, "design.background", json!({"pattern": {"preset": "pct20", "fg": "accent1", "bg": "bg1"}})), @@ -432,7 +432,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { && let Some(path) = pick("picture") && let Some(Ok(bytes)) = app.services.read.as_ref().map(|r| r(&path)) { - let _ = app.run("design.background", json!({"picture": slidecraft_engine::cmd::base64_encode(&bytes)})); + let _ = app.run("design.background", json!({"picture": deckcraft_engine::cmd::base64_encode(&bytes)})); } } _ => {} @@ -672,7 +672,7 @@ fn designer(app: &mut SlideApp, ui: &mut Ui) { let doc = st.doc.clone(); let idx = st.selection.slide; let w = ui.available_width() - 8.0; - for th in slidecraft_model::theme::builtin_themes() { + for th in deckcraft_model::theme::builtin_themes() { let mut d = (*doc).clone(); if let Some(m) = d.masters.first_mut() { let m = std::sync::Arc::make_mut(m); @@ -681,10 +681,10 @@ fn designer(app: &mut SlideApp, ui: &mut Ui) { let ppp = ui.ctx().pixels_per_point(); let px = (w * ppp) as u32; let h = (px as f64 * d.slide_size.height / d.slide_size.width.max(1.0)) as u32; - let img = slidecraft_render::render_slide( + let img = deckcraft_render::render_slide( &d, idx, - &slidecraft_render::RenderOpts { scale: px as f64 / d.slide_size.width.max(1.0), size: Some((px, h)), ..Default::default() }, + &deckcraft_render::RenderOpts { scale: px as f64 / d.slide_size.width.max(1.0), size: Some((px, h)), ..Default::default() }, ); let tex = ui.ctx().load_texture(format!("idea-{}", th.name), crate::textures::to_color_image(&img, false), egui::TextureOptions::LINEAR); let (r, resp) = ui.allocate_exact_size(vec2(w, w * h as f32 / px.max(1) as f32), Sense::click()); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 2dc7eaa..3f063ca 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1,9 +1,9 @@ //! Title bar, ribbon tabs and the ribbon card with every tab's groups. +use deckcraft_engine::Target; +use deckcraft_model::{ShapeKind, anim::AnimClass}; use egui::{Align2, Color32, CornerRadius, Rect, Sense, Stroke, Ui, pos2, vec2}; use serde_json::{Value, json}; -use slidecraft_engine::Target; -use slidecraft_model::{ShapeKind, anim::AnimClass}; use crate::SlideApp; use crate::icons::{self, Icon}; @@ -14,7 +14,7 @@ pub const TITLE_H: f32 = 38.0; /// Is engine command `id` enabled right now? pub fn enabled(app: &SlideApp, id: &str) -> bool { - match slidecraft_engine::find_command(id) { + match deckcraft_engine::find_command(id) { Some(c) => (c.enabled)(&app.session).is_ok(), None => crate::UI_COMMANDS.iter().any(|c| c.0 == id), } @@ -63,7 +63,7 @@ pub fn title_bar(app: &mut SlideApp, ui: &mut Ui) { app.start_show(0, false); } let title = - app.session.active().map(|d| format!("{}{}", d.title(), if dirty { " — Edited" } else { "" })).unwrap_or_else(|| "SlideCraft".into()); + app.session.active().map(|d| format!("{}{}", d.title(), if dirty { " — Edited" } else { "" })).unwrap_or_else(|| "DeckCraft".into()); ui.painter().text(rect.center(), Align2::CENTER_CENTER, title, theme::font(13.0), t.text); // Right side. let mut right = ui.new_child( @@ -234,7 +234,7 @@ fn toggle(app: &mut SlideApp, ui: &mut Ui, icon: Icon, label: &str, id: &str, on } fn tip(id: &str, label: &str) -> String { - let sc = slidecraft_engine::find_command(id).and_then(|c| c.shortcut).or_else(|| crate::UI_COMMANDS.iter().find(|c| c.0 == id).and_then(|c| c.2)); + let sc = deckcraft_engine::find_command(id).and_then(|c| c.shortcut).or_else(|| crate::UI_COMMANDS.iter().find(|c| c.0 == id).and_then(|c| c.2)); match sc { Some(s) => format!("{label} ({})", pretty_shortcut(s)), None => label.to_string(), @@ -249,8 +249,8 @@ pub fn pretty_shortcut(s: &str) -> String { } } -fn scheme(app: &SlideApp) -> slidecraft_color::ColorScheme { - app.session.active().and_then(|d| d.doc.masters.first().map(|m| m.scheme())).unwrap_or_else(|| slidecraft_model::Theme::default().colors) +fn scheme(app: &SlideApp) -> deckcraft_color::ColorScheme { + app.session.active().and_then(|d| d.doc.masters.first().map(|m| m.scheme())).unwrap_or_else(|| deckcraft_model::Theme::default().colors) } /// The formatting state at the selection (bold, size…). @@ -302,7 +302,7 @@ fn clipboard_group(app: &mut SlideApp, ui: &mut Ui) { } fn layout_menu(app: &mut SlideApp, ui: &mut Ui, cmd: &str) { - let layouts: Vec<(String, slidecraft_model::LayoutId)> = app + let layouts: Vec<(String, deckcraft_model::LayoutId)> = app .session .active() .and_then(|d| d.doc.masters.first().map(|m| m.layouts.iter().map(|l| (l.name.clone(), l.id)).collect())) @@ -313,11 +313,11 @@ fn layout_menu(app: &mut SlideApp, ui: &mut Ui, cmd: &str) { let resp = ui.vertical(|ui| { let (rect, resp) = ui.allocate_exact_size(vec2(96.0, 54.0), Sense::click()); if let (Some(d), Some(li)) = (app.session.active(), idx) { - let img = slidecraft_render::render_layout( + let img = deckcraft_render::render_layout( &d.doc, 0, Some(li), - &slidecraft_render::RenderOpts { + &deckcraft_render::RenderOpts { scale: 192.0 / d.doc.slide_size.width.max(1.0), edit: true, size: Some((192, 108)), @@ -390,7 +390,7 @@ pub fn font_families(app: &SlideApp) -> Vec { v.push(m.theme.fonts.major.latin.clone()); v.push(m.theme.fonts.minor.latin.clone()); } - v.extend(slidecraft_fonts::FontDb::global().families()); + v.extend(deckcraft_fonts::FontDb::global().families()); v.dedup(); v } @@ -425,7 +425,7 @@ fn font_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { }); let size_text = size.map(|s| widgets::fmt_num(s, "")).unwrap_or_default(); dropdown(ui, "size", &size_text, 52.0, en, |ui| { - for s in slidecraft_engine::cmd::format::SIZES.iter().chain([72.0, 80.0, 88.0, 96.0].iter()) { + for s in deckcraft_engine::cmd::format::SIZES.iter().chain([72.0, 80.0, 88.0, 96.0].iter()) { if ui.button(widgets::fmt_num(*s, "")).clicked() { run(app, "format.size", json!({"size": s})); ui.close(); @@ -493,9 +493,9 @@ fn font_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { } /// A ColorRef as a command colour parameter. -pub fn cref_param(c: &slidecraft_model::ColorRef) -> Value { +pub fn cref_param(c: &deckcraft_model::ColorRef) -> Value { match &c.base { - slidecraft_model::ColorBase::Scheme { slot } => { + deckcraft_model::ColorBase::Scheme { slot } => { let mut o = serde_json::Map::new(); o.insert("scheme".into(), json!(slot.xml_name())); for m in &c.mods { @@ -505,8 +505,8 @@ pub fn cref_param(c: &slidecraft_model::ColorRef) -> Value { } Value::Object(o) } - slidecraft_model::ColorBase::Rgb { rgb } if c.mods.is_empty() => json!(format!("#{}", rgb.hex())), - slidecraft_model::ColorBase::Rgb { rgb } => { + deckcraft_model::ColorBase::Rgb { rgb } if c.mods.is_empty() => json!(format!("#{}", rgb.hex())), + deckcraft_model::ColorBase::Rgb { rgb } => { let mut o = serde_json::Map::new(); o.insert("rgb".into(), json!(format!("#{}", rgb.hex()))); for m in &c.mods { @@ -629,11 +629,11 @@ pub fn shapes_gallery(ui: &mut Ui) -> Option<&'static str> { let mut out = None; ui.set_max_width(330.0); egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| { - for cat in slidecraft_geom::preset::Category::ALL { + for cat in deckcraft_geom::preset::Category::ALL { ui.label(egui::RichText::new(cat.label()).font(theme::bold(12.0))); ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing = vec2(1.0, 1.0); - for p in slidecraft_geom::preset::CATALOG.iter().filter(|p| p.category == cat) { + for p in deckcraft_geom::preset::CATALOG.iter().filter(|p| p.category == cat) { let (r, resp) = ui.allocate_exact_size(vec2(26.0, 26.0), Sense::click()); if resp.hovered() { ui.painter().rect_filled(r, CornerRadius::same(3), t.hover); @@ -643,8 +643,8 @@ pub fn shapes_gallery(ui: &mut Ui) -> Option<&'static str> { out = Some(p.name); } } - if cat == slidecraft_geom::preset::Category::Lines { - for (name, label) in slidecraft_engine::tools::FREEFORM_TOOLS { + if cat == deckcraft_geom::preset::Category::Lines { + for (name, label) in deckcraft_engine::tools::FREEFORM_TOOLS { let (r, resp) = ui.allocate_exact_size(vec2(26.0, 26.0), Sense::click()); if resp.hovered() { ui.painter().rect_filled(r, CornerRadius::same(3), t.hover); @@ -668,7 +668,7 @@ pub fn shapes_gallery(ui: &mut Ui) -> Option<&'static str> { /// Draw a preset's outline (gallery icons). pub fn paint_preset(p: &egui::Painter, r: Rect, name: &str, fill: Color32, stroke: Color32) { let (w, h) = (r.width() as f64, r.height() as f64); - let Some(g) = slidecraft_geom::preset::build(name, w, h, &[]) else { return }; + let Some(g) = deckcraft_geom::preset::build(name, w, h, &[]) else { return }; for sp in &g.paths { let mut pts: Vec = vec![]; let mut polys: Vec<(Vec, bool)> = vec![]; @@ -691,7 +691,7 @@ pub fn paint_preset(p: &egui::Painter, r: Rect, name: &str, fill: Color32, strok polys.push((pts, false)); } for (poly, closed) in polys { - if closed && fill != Color32::TRANSPARENT && sp.fill != slidecraft_geom::preset::FillMode::None { + if closed && fill != Color32::TRANSPARENT && sp.fill != deckcraft_geom::preset::FillMode::None { p.add(egui::Shape::Path(egui::epaint::PathShape { points: poly.clone(), closed: true, fill, stroke: Stroke::NONE.into() })); } if sp.stroke { @@ -712,11 +712,11 @@ enum Fl { Close, } -fn kurbo_flatten(path: &slidecraft_geom::BezPath, mut f: impl FnMut(Fl)) { - slidecraft_geom::preset_flatten(path, &mut |el| match el { - slidecraft_geom::PathEl::MoveTo(p) => f(Fl::Move(p.x, p.y)), - slidecraft_geom::PathEl::LineTo(p) => f(Fl::Line(p.x, p.y)), - slidecraft_geom::PathEl::ClosePath => f(Fl::Close), +fn kurbo_flatten(path: &deckcraft_geom::BezPath, mut f: impl FnMut(Fl)) { + deckcraft_geom::preset_flatten(path, &mut |el| match el { + deckcraft_geom::PathEl::MoveTo(p) => f(Fl::Move(p.x, p.y)), + deckcraft_geom::PathEl::LineTo(p) => f(Fl::Line(p.x, p.y)), + deckcraft_geom::PathEl::ClosePath => f(Fl::Close), _ => {} }); } @@ -727,7 +727,7 @@ fn drawing_group(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::Shapes, "Shapes", en).on_hover_text("Shapes"); egui::Popup::menu(&r).show(|ui| { if let Some(p) = shapes_gallery(ui) { - app.session.set_tool(slidecraft_engine::ToolKind::Shape { preset: p.to_string() }); + app.session.set_tool(deckcraft_engine::ToolKind::Shape { preset: p.to_string() }); } }); big(app, ui, Icon::TextBox, "Text Box", "insert.textBox", json!({})); @@ -767,7 +767,7 @@ fn drawing_group(app: &mut SlideApp, ui: &mut Ui) { }); } -pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &slidecraft_color::ColorScheme) { +pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::ColorScheme) { if let Some(c) = widgets::color_grid(ui, sc, Some("No Outline")) { match c { Some(c) => run(app, "shape.line", json!({"color": cref_param(&c)})), @@ -880,13 +880,13 @@ pub fn effects_menu(app: &mut SlideApp, ui: &mut Ui) { fn quick_styles(app: &mut SlideApp, ui: &mut Ui) { let sc = scheme(app); let slots = [ - slidecraft_color::SchemeSlot::Dk1, - slidecraft_color::SchemeSlot::Accent1, - slidecraft_color::SchemeSlot::Accent2, - slidecraft_color::SchemeSlot::Accent3, - slidecraft_color::SchemeSlot::Accent4, - slidecraft_color::SchemeSlot::Accent5, - slidecraft_color::SchemeSlot::Accent6, + deckcraft_color::SchemeSlot::Dk1, + deckcraft_color::SchemeSlot::Accent1, + deckcraft_color::SchemeSlot::Accent2, + deckcraft_color::SchemeSlot::Accent3, + deckcraft_color::SchemeSlot::Accent4, + deckcraft_color::SchemeSlot::Accent5, + deckcraft_color::SchemeSlot::Accent6, ]; egui::Grid::new("quickstyles").spacing(vec2(4.0, 4.0)).show(ui, |ui| { for row in 0..6 { @@ -1020,7 +1020,7 @@ fn insert(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::Shapes, "Shapes", enabled(app, "shape.insert")); egui::Popup::menu(&r).show(|ui| { if let Some(p) = shapes_gallery(ui) { - app.session.set_tool(slidecraft_engine::ToolKind::Shape { preset: p.to_string() }); + app.session.set_tool(deckcraft_engine::ToolKind::Shape { preset: p.to_string() }); } }); let r = big_button(ui, Icon::Icons, "Icons", enabled(app, "shape.insert")); @@ -1028,7 +1028,7 @@ fn insert(app: &mut SlideApp, ui: &mut Ui) { big(app, ui, Icon::SmartArt, "SmartArt", "app.dialog", json!({"id": "smartart"})); let r = big_button(ui, Icon::Chart, "Chart", enabled(app, "insert.chart")); egui::Popup::menu(&r).show(|ui| { - for k in slidecraft_model::chart::ChartType::MAIN { + for k in deckcraft_model::chart::ChartType::MAIN { if ui.button(k.label()).clicked() { run(app, "insert.chart", json!({"type": format!("{k:?}")})); } @@ -1141,7 +1141,7 @@ fn icon_library(app: &mut SlideApp, ui: &mut Ui) { } paint_preset(ui.painter(), r.shrink(6.0), preset, t.text, t.text); if resp.on_hover_text(label).clicked() { - let size = app.session.active().map(|d| d.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE); + let size = app.session.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE); let _ = app.run("shape.insert", json!({"preset": preset, "rect": [size.width / 2.0 - 48.0, size.height / 2.0 - 48.0, 96, 96]})); let _ = app.run("shape.fill", json!({"color": "tx1"})); let _ = app.run("shape.line", json!({"none": true})); @@ -1154,7 +1154,7 @@ fn icon_library(app: &mut SlideApp, ui: &mut Ui) { // ---------- Draw ---------- fn draw(app: &mut SlideApp, ui: &mut Ui) { - use slidecraft_engine::ToolKind; + use deckcraft_engine::ToolKind; let cur = app.session.tool.kind.clone(); group(ui, |ui| { let sel = matches!(cur, ToolKind::Select); @@ -1163,16 +1163,16 @@ fn draw(app: &mut SlideApp, ui: &mut Ui) { } let er = matches!(&cur, ToolKind::Ink { mode, .. } if mode == "eraser"); if big_button_ex(ui, Icon::Eraser, "Eraser", true, false, er).0.clicked() { - app.session.set_tool(ToolKind::Ink { mode: "eraser".into(), color: slidecraft_color::Rgba::BLACK, width: 2.0 }); + app.session.set_tool(ToolKind::Ink { mode: "eraser".into(), color: deckcraft_color::Rgba::BLACK, width: 2.0 }); } }); group(ui, |ui| { - let pens: [(Icon, &str, &str, slidecraft_color::Rgba, f64); 5] = [ - (Icon::Pen, "Black Pen", "pen", slidecraft_color::Rgba::rgb(20, 20, 20), 1.5), - (Icon::Pen, "Red Pen", "pen", slidecraft_color::Rgba::rgb(0xD8, 0x3F, 0x3F), 1.5), - (Icon::Pen, "Blue Pen", "pen", slidecraft_color::Rgba::rgb(0x2E, 0x6F, 0xD8), 1.5), - (Icon::Pencil, "Pencil", "pen", slidecraft_color::Rgba::rgb(90, 90, 90), 1.0), - (Icon::HighlighterPen, "Highlighter", "highlighter", slidecraft_color::Rgba::rgb(255, 230, 0), 10.0), + let pens: [(Icon, &str, &str, deckcraft_color::Rgba, f64); 5] = [ + (Icon::Pen, "Black Pen", "pen", deckcraft_color::Rgba::rgb(20, 20, 20), 1.5), + (Icon::Pen, "Red Pen", "pen", deckcraft_color::Rgba::rgb(0xD8, 0x3F, 0x3F), 1.5), + (Icon::Pen, "Blue Pen", "pen", deckcraft_color::Rgba::rgb(0x2E, 0x6F, 0xD8), 1.5), + (Icon::Pencil, "Pencil", "pen", deckcraft_color::Rgba::rgb(90, 90, 90), 1.0), + (Icon::HighlighterPen, "Highlighter", "highlighter", deckcraft_color::Rgba::rgb(255, 230, 0), 10.0), ]; for (icon, label, mode, color, width) in pens { let on = matches!(&cur, ToolKind::Ink { mode: m, color: c, .. } if m == mode && *c == color); @@ -1194,7 +1194,7 @@ fn draw(app: &mut SlideApp, ui: &mut Ui) { // ---------- Design ---------- fn design(app: &mut SlideApp, ui: &mut Ui) { - let themes = slidecraft_model::theme::builtin_themes(); + let themes = deckcraft_model::theme::builtin_themes(); let cur = app.session.active().and_then(|d| d.doc.masters.first().map(|m| m.theme.name.clone())).unwrap_or_default(); group(ui, |ui| { let sel = themes.iter().position(|th| th.name == cur); @@ -1218,7 +1218,7 @@ fn design(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::Colors, "Colors", true); egui::Popup::menu(&r).width(240.0).show(|ui| { - for cs in slidecraft_model::theme::builtin_color_schemes() { + for cs in deckcraft_model::theme::builtin_color_schemes() { ui.horizontal(|ui| { let (rect, resp) = ui.allocate_exact_size(vec2(220.0, 20.0), Sense::click()); let p = ui.painter(); @@ -1239,7 +1239,7 @@ fn design(app: &mut SlideApp, ui: &mut Ui) { }); let r = big_button(ui, Icon::Fonts, "Fonts", true); egui::Popup::menu(&r).width(220.0).show(|ui| { - for fs in slidecraft_model::theme::builtin_font_schemes() { + for fs in deckcraft_model::theme::builtin_font_schemes() { if ui.button(format!("{}\n {} / {}", fs.name, fs.major.latin, fs.minor.latin)).clicked() { run(app, "design.fonts", json!({"name": fs.name})); } @@ -1251,10 +1251,10 @@ fn design(app: &mut SlideApp, ui: &mut Ui) { let sc = scheme(app); for i in 1..=12 { let slots = [ - slidecraft_color::SchemeSlot::Lt1, - slidecraft_color::SchemeSlot::Lt2, - slidecraft_color::SchemeSlot::Dk2, - slidecraft_color::SchemeSlot::Dk1, + deckcraft_color::SchemeSlot::Lt1, + deckcraft_color::SchemeSlot::Lt2, + deckcraft_color::SchemeSlot::Dk2, + deckcraft_color::SchemeSlot::Dk1, ]; let base = theme::to_color32(sc.get(slots[(i - 1) % 4])); let shade = match (i - 1) / 4 { @@ -1298,14 +1298,14 @@ fn design(app: &mut SlideApp, ui: &mut Ui) { }); } -pub fn paint_theme_tile(p: &egui::Painter, r: Rect, th: &slidecraft_model::Theme) { - let bg = theme::to_color32(th.colors.get(slidecraft_color::SchemeSlot::Lt1)); - let fg = theme::to_color32(th.colors.get(slidecraft_color::SchemeSlot::Dk1)); +pub fn paint_theme_tile(p: &egui::Painter, r: Rect, th: &deckcraft_model::Theme) { + let bg = theme::to_color32(th.colors.get(deckcraft_color::SchemeSlot::Lt1)); + let fg = theme::to_color32(th.colors.get(deckcraft_color::SchemeSlot::Dk1)); p.rect(r, CornerRadius::same(2), bg, Stroke::new(1.0, Color32::from_gray(200)), egui::StrokeKind::Inside); p.text(r.min + vec2(8.0, r.height() * 0.42), Align2::LEFT_CENTER, "Aa", theme::font(r.height() * 0.38), fg); let w = (r.width() - 16.0) / 6.0; for k in 0..6 { - let c = theme::to_color32(th.colors.colors.get(4 + k).copied().unwrap_or(slidecraft_color::Rgba::BLACK)); + let c = theme::to_color32(th.colors.colors.get(4 + k).copied().unwrap_or(deckcraft_color::Rgba::BLACK)); p.rect_filled(Rect::from_min_size(pos2(r.min.x + 8.0 + k as f32 * w, r.max.y - 11.0), vec2(w - 1.5, 5.0)), CornerRadius::ZERO, c); } } @@ -1325,7 +1325,7 @@ fn transitions(app: &mut SlideApp, ui: &mut Ui) { } }); group(ui, |ui| { - let list = slidecraft_model::anim::TRANSITIONS; + let list = deckcraft_model::anim::TRANSITIONS; let shown: Vec<_> = list.iter().take(12).collect(); let sel = shown.iter().position(|x| x.0 == kind); if let Some(i) = widgets::gallery( @@ -1360,7 +1360,7 @@ fn transitions(app: &mut SlideApp, ui: &mut Ui) { }); }); group(ui, |ui| { - let opts: &[&str] = slidecraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == kind).map(|x| x.4).unwrap_or(&[]); + let opts: &[&str] = deckcraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == kind).map(|x| x.4).unwrap_or(&[]); let r = big_button(ui, Icon::EffectOptions, "Effect\nOptions", !opts.is_empty()); egui::Popup::menu(&r).show(|ui| { for o in opts { @@ -1499,7 +1499,7 @@ pub fn paint_transition_tile(p: &egui::Painter, r: Rect, (id, label): (&str, &st fn animations(app: &mut SlideApp, ui: &mut Ui) { let has_sel = app.session.active().is_some_and(|d| !d.selection.shapes.is_empty()); - let cur: Option = app.session.active().and_then(|d| { + let cur: Option = app.session.active().and_then(|d| { let s = d.current_slide()?; let id = d.selection.shapes.first()?; s.animations.iter().find(|a| a.shape == *id).cloned() @@ -1515,7 +1515,7 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { } }); group(ui, |ui| { - let quick: Vec<_> = slidecraft_model::anim::ANIMATIONS + let quick: Vec<_> = deckcraft_model::anim::ANIMATIONS .iter() .filter(|a| { matches!( @@ -1559,7 +1559,7 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { { ui.label(egui::RichText::new(label).font(theme::bold(12.0))); ui.horizontal_wrapped(|ui| { - for a in slidecraft_model::anim::ANIMATIONS.iter().filter(|a| a.2 == cls) { + for a in deckcraft_model::anim::ANIMATIONS.iter().filter(|a| a.2 == cls) { let (r, resp) = ui.allocate_exact_size(vec2(56.0, 54.0), Sense::click()); if resp.hovered() { ui.painter().rect_filled(r, CornerRadius::same(4), Tokens::get(ui.ctx()).hover); @@ -1573,7 +1573,7 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { }); } }); - let opts: &[&str] = cur.as_ref().and_then(|c| slidecraft_model::anim::animation_info(&c.effect, c.class)).map(|x| x.5).unwrap_or(&[]); + let opts: &[&str] = cur.as_ref().and_then(|c| deckcraft_model::anim::animation_info(&c.effect, c.class)).map(|x| x.5).unwrap_or(&[]); let r = big_button(ui, Icon::EffectOptions, "Effect\nOptions", !opts.is_empty()); egui::Popup::menu(&r).show(|ui| { for o in opts { @@ -1592,7 +1592,7 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::AddAnimation, "Add\nAnimation", has_sel); egui::Popup::menu(&r).width(260.0).show(|ui| { - for a in slidecraft_model::anim::ANIMATIONS.iter().filter(|a| a.2 != AnimClass::Media) { + for a in deckcraft_model::anim::ANIMATIONS.iter().filter(|a| a.2 != AnimClass::Media) { if ui .button(format!( "{} — {}", @@ -1630,9 +1630,9 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.label("Start:"); let s = cur.as_ref().map(|c| match c.start { - slidecraft_model::AnimStart::OnClick => "On Click", - slidecraft_model::AnimStart::WithPrevious => "With Previous", - slidecraft_model::AnimStart::AfterPrevious => "After Previous", + deckcraft_model::AnimStart::OnClick => "On Click", + deckcraft_model::AnimStart::WithPrevious => "With Previous", + deckcraft_model::AnimStart::AfterPrevious => "After Previous", }); dropdown(ui, "astart", s.unwrap_or(""), 112.0, cur.is_some(), |ui| { for (l, v) in [("On Click", "onClick"), ("With Previous", "withPrevious"), ("After Previous", "afterPrevious")] { @@ -1741,7 +1741,7 @@ fn record(app: &mut SlideApp, ui: &mut Ui) { }); group(ui, |ui| { if big_button(ui, Icon::Help, "Learn\nMore", true).clicked() { - app.open_url(slidecraft_engine::links::APP_PAGE); + app.open_url(deckcraft_engine::links::APP_PAGE); } }); } @@ -1850,7 +1850,7 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { let r = big_button(ui, Icon::Shapes, "Shapes", true); egui::Popup::menu(&r).show(|ui| { if let Some(p) = shapes_gallery(ui) { - app.session.set_tool(slidecraft_engine::ToolKind::Shape { preset: p.to_string() }); + app.session.set_tool(deckcraft_engine::ToolKind::Shape { preset: p.to_string() }); } }); big(app, ui, Icon::TextBox, "Text\nBox", "insert.textBox", json!({})); @@ -1864,7 +1864,7 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { let can_merge = enabled(app, "shape.merge"); let r = big_button(ui, Icon::MergeShapes, "Merge\nShapes", can_merge).on_hover_text("Merge Shapes"); egui::Popup::menu(&r).show(|ui| { - for (op, label) in slidecraft_engine::cmd::merge::OPS { + for (op, label) in deckcraft_engine::cmd::merge::OPS { if ui.button(label).clicked() { run(app, "shape.merge", json!({"op": op})); ui.close(); @@ -1991,7 +1991,7 @@ fn size_group(app: &mut SlideApp, ui: &mut Ui) { let bx = app .session .active() - .and_then(|d| d.selection.shapes.first().and_then(|id| d.shape(*id).map(|s| slidecraft_engine::cmd::xfrm_of(&d.doc, &d.selection, s)))); + .and_then(|d| d.selection.shapes.first().and_then(|id| d.shape(*id).map(|s| deckcraft_engine::cmd::xfrm_of(&d.doc, &d.selection, s)))); group(ui, |ui| { rows(ui, |ui| { let Some(x) = bx else { @@ -2143,7 +2143,7 @@ fn table_design(app: &mut SlideApp, ui: &mut Ui) { }); }); group(ui, |ui| { - let styles = slidecraft_model::table::table_styles(); + let styles = deckcraft_model::table::table_styles(); let quick: Vec<_> = styles.iter().filter(|(id, _)| id.starts_with("medium2") || id == "none" || id == "grid").take(8).collect(); let cur = tbl.as_ref().map(|t| t.style.clone()).unwrap_or_default(); let sel = quick.iter().position(|(id, _)| *id == cur); @@ -2194,15 +2194,15 @@ fn table_design(app: &mut SlideApp, ui: &mut Ui) { }); } -pub fn paint_table_style(p: &egui::Painter, r: Rect, id: &str, sc: &slidecraft_color::ColorScheme) { +pub fn paint_table_style(p: &egui::Painter, r: Rect, id: &str, sc: &deckcraft_color::ColorScheme) { let slot = match id.rsplit('-').next().unwrap_or("accent1") { - "accent2" => slidecraft_color::SchemeSlot::Accent2, - "accent3" => slidecraft_color::SchemeSlot::Accent3, - "accent4" => slidecraft_color::SchemeSlot::Accent4, - "accent5" => slidecraft_color::SchemeSlot::Accent5, - "accent6" => slidecraft_color::SchemeSlot::Accent6, - "tx1" => slidecraft_color::SchemeSlot::Dk1, - _ => slidecraft_color::SchemeSlot::Accent1, + "accent2" => deckcraft_color::SchemeSlot::Accent2, + "accent3" => deckcraft_color::SchemeSlot::Accent3, + "accent4" => deckcraft_color::SchemeSlot::Accent4, + "accent5" => deckcraft_color::SchemeSlot::Accent5, + "accent6" => deckcraft_color::SchemeSlot::Accent6, + "tx1" => deckcraft_color::SchemeSlot::Dk1, + _ => deckcraft_color::SchemeSlot::Accent1, }; let a = theme::to_color32(sc.get(slot)); let kind = id.split('-').next().unwrap_or(""); @@ -2272,7 +2272,7 @@ fn chart_design(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::ChangeChartType, "Change\nChart Type", true); egui::Popup::menu(&r).show(|ui| { - for k in slidecraft_model::chart::ChartType::MAIN { + for k in deckcraft_model::chart::ChartType::MAIN { if ui.button(k.label()).clicked() { // Replace the chart's type keeping its data. if let Some(id) = app.session.active().and_then(|d| d.selection.shapes.first().copied()) { @@ -2361,7 +2361,7 @@ fn slide_master(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { let r = big_button(ui, Icon::Themes, "Themes", true); egui::Popup::menu(&r).show(|ui| { - for th in slidecraft_model::theme::builtin_themes() { + for th in deckcraft_model::theme::builtin_themes() { if ui.button(&th.name).clicked() { run(app, "design.theme", json!({"name": th.name})); } diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 610d652..bba6ec2 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -1,11 +1,11 @@ -//! Slide Show: full-screen playback with transitions and animations (`slidecraft-anim`), +//! Slide Show: full-screen playback with transitions and animations (`deckcraft-anim`), //! keyboard/mouse navigation, pen, black/white screens, presenter view and rehearsed timings. +use deckcraft_anim::{Layer, ShowAction, ShowState, Source, Timeline}; +use deckcraft_model::{Presentation, ShapeId}; +use deckcraft_render::{ParaState, RenderOpts, ShapeState}; use egui::{Align2, Color32, CornerRadius, Mesh, Pos2, Rect, Sense, Stroke, TextureHandle, Ui, pos2, vec2}; use serde_json::json; -use slidecraft_anim::{Layer, ShowAction, ShowState, Source, Timeline}; -use slidecraft_model::{Presentation, ShapeId}; -use slidecraft_render::{ParaState, RenderOpts, ShapeState}; use crate::SlideApp; use crate::theme; @@ -162,7 +162,7 @@ fn state_for(tl: &Timeline, id: ShapeId, step: usize, t: f64) -> ShapeState { ShapeState { visible: a.visible, opacity: a.opacity, - offset: slidecraft_geom::Vec2::new(a.offset_x, a.offset_y), + offset: deckcraft_geom::Vec2::new(a.offset_x, a.offset_y), scale: (a.scale_x, a.scale_y), rotate: a.rotate, clip: a.clip, @@ -172,7 +172,7 @@ fn state_for(tl: &Timeline, id: ShapeId, step: usize, t: f64) -> ShapeState { .into_iter() .map(|p| { let a = tl.state(id, Some(p), step, t); - (p, ParaState { visible: a.visible, opacity: a.opacity, offset: slidecraft_geom::Vec2::new(a.offset_x, a.offset_y) }) + (p, ParaState { visible: a.visible, opacity: a.opacity, offset: deckcraft_geom::Vec2::new(a.offset_x, a.offset_y) }) }) .collect(), } @@ -341,7 +341,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { let hidden: Vec = media .iter() .filter(|(id, _, clip)| { - clip.hide_while_not_playing && !app.media.status(*id).is_some_and(|s| s.state != slidecraft_media::PlayState::Ended) + clip.hide_while_not_playing && !app.media.status(*id).is_some_and(|s| s.state != deckcraft_media::PlayState::Ended) }) .map(|m| m.0) .collect(); @@ -360,7 +360,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { }; let threads = if cfg!(target_arch = "wasm32") { 0 } else { std::thread::available_parallelism().map(|n| n.get().min(8) as u16).unwrap_or(0) }; - let img = slidecraft_render::render_slide( + let img = deckcraft_render::render_slide( &doc, idx, &RenderOpts { @@ -386,7 +386,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { if let (Some(tr), Some(new)) = (&show.trans, &tex) { let p = ((now - tr.start) / tr.dur.max(0.01)).clamp(0.0, 1.0); if p < 1.0 { - for l in slidecraft_anim::transition_layers(&tr.kind, &tr.option, p) { + for l in deckcraft_anim::transition_layers(&tr.kind, &tr.option, p) { let mesh = match l.source { Source::Old => match &tr.old { Some(o) => quad_mesh(Some(o.id()), srect, &l, Color32::WHITE), @@ -572,7 +572,7 @@ fn show_media(app: &mut SlideApp, show: &mut Show, doc: &Presentation) { } /// The media shape under the pointer on the shown slide. -fn media_hit(ctx: &egui::Context, ui: &Ui, show: &Show, doc: &Presentation) -> Option<(ShapeId, slidecraft_model::MediaClip)> { +fn media_hit(ctx: &egui::Context, ui: &Ui, show: &Show, doc: &Presentation) -> Option<(ShapeId, deckcraft_model::MediaClip)> { let p = ctx.input(|i| i.pointer.interact_pos())?; let full = ui.max_rect(); let avail = if show.reading { Rect::from_min_max(full.min, pos2(full.max.x, full.max.y - 34.0)) } else { full }; @@ -596,12 +596,12 @@ impl Show { fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64, keep: &mut bool) { let cur = show.state.slide; - let next = slidecraft_anim::show_order(doc).into_iter().find(|i| *i > cur); + let next = deckcraft_anim::show_order(doc).into_iter().find(|i| *i > cur); let cur_tex = show.tex.as_ref().map(|(_, t)| t.clone()); if let Some(n) = next && show.next_tex.as_ref().is_none_or(|(k, _)| *k != n) { - let img = slidecraft_render::render_slide( + let img = deckcraft_render::render_slide( doc, n, &RenderOpts { @@ -617,8 +617,8 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no let elapsed = now - show.show_start; let mut actions = vec![]; ctx.show_viewport_immediate( - egui::ViewportId::from_hash_of("slidecraft_presenter"), - egui::ViewportBuilder::default().with_title("Presenter View — SlideCraft").with_inner_size([1100.0, 700.0]), + egui::ViewportId::from_hash_of("deckcraft_presenter"), + egui::ViewportBuilder::default().with_title("Presenter View — DeckCraft").with_inner_size([1100.0, 700.0]), |ui, _| { egui::CentralPanel::default().frame(egui::Frame::NONE.fill(Color32::from_gray(24)).inner_margin(egui::Margin::same(16))).show(ui, |ui| { ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/status.rs b/crates/ui-egui/src/status.rs index 7a7b159..a6f4eb1 100644 --- a/crates/ui-egui/src/status.rs +++ b/crates/ui-egui/src/status.rs @@ -15,7 +15,7 @@ pub fn status_bar(app: &mut SlideApp, ui: &mut Ui) { let Some(st) = app.session.active() else { return }; let n = st.doc.slides.len(); let left = match st.selection.target { - slidecraft_engine::Target::Slides => format!("Slide {} of {}", (st.selection.slide + 1).min(n.max(1)), n), + deckcraft_engine::Target::Slides => format!("Slide {} of {}", (st.selection.slide + 1).min(n.max(1)), n), _ => "Slide Master".to_string(), }; let p = ui.painter(); @@ -112,7 +112,7 @@ fn labeled(ui: &mut Ui, icon: Icon, label: &str, on: bool) -> egui::Response { pub fn notes_pane(app: &mut SlideApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { return }; - if st.selection.target != slidecraft_engine::Target::Slides { + if st.selection.target != deckcraft_engine::Target::Slides { return; } let idx = st.selection.slide; diff --git a/crates/ui-egui/src/textures.rs b/crates/ui-egui/src/textures.rs index aa21913..b81360f 100644 --- a/crates/ui-egui/src/textures.rs +++ b/crates/ui-egui/src/textures.rs @@ -4,9 +4,9 @@ use std::collections::HashMap; use std::sync::Arc; +use deckcraft_model::{Master, Presentation, Slide}; +use deckcraft_render::{Image, RenderOpts}; use egui::{ColorImage, TextureHandle, TextureOptions}; -use slidecraft_model::{Master, Presentation, Slide}; -use slidecraft_render::{Image, RenderOpts}; #[derive(Clone, PartialEq)] struct Key { @@ -87,7 +87,7 @@ impl Textures { } let t0 = crate::now_ms(); let scale = size.0 as f64 / p.slide_size.width.max(1.0); - let img = slidecraft_render::render_slide(p, index, &RenderOpts { scale, edit, threads: threads(), size: Some(size), ..Default::default() }); + let img = deckcraft_render::render_slide(p, index, &RenderOpts { scale, edit, threads: threads(), size: Some(size), ..Default::default() }); self.last_render_ms = crate::now_ms() - t0; let ci = to_color_image(&img, grayscale); let tex = match self.canvas.take() { @@ -134,7 +134,7 @@ impl Textures { } *budget -= 1; let scale = width_px as f64 / p.slide_size.width.max(1.0); - let img = slidecraft_render::render_slide(p, index, &RenderOpts { scale, size: Some((width_px, h)), threads: 0, ..Default::default() }); + let img = deckcraft_render::render_slide(p, index, &RenderOpts { scale, size: Some((width_px, h)), threads: 0, ..Default::default() }); let tex = ctx.load_texture(format!("thumb-{index}"), to_color_image(&img, false), TextureOptions::LINEAR); // Drop the old entry for this slide address; entries for slides that no longer exist age out. self.thumbs.insert(key.slide, Entry { key, tex: tex.clone(), _slide: slide, _master: master, stamp: clock }); diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index b45eb53..0016714 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -30,7 +30,7 @@ pub struct Tokens { pub hover: Color32, pub pressed: Color32, pub selected: Color32, - /// SlideCraft's accent (tabs underline, selected thumbnail, primary buttons). + /// DeckCraft's accent (tabs underline, selected thumbnail, primary buttons). pub accent: Color32, pub accent_text: Color32, pub contextual: Color32, @@ -157,7 +157,7 @@ pub fn apply(ctx: &egui::Context, t: &Tokens) { pub fn install_fonts(ctx: &egui::Context) { let mut defs = FontDefinitions::default(); let mut names = vec![]; - for (i, f) in slidecraft_fonts::CRAFT_FONTS.iter().enumerate() { + for (i, f) in deckcraft_fonts::CRAFT_FONTS.iter().enumerate() { let wanted = (f.family == "Inter" && (f.style == "Regular" || f.style == "SemiBold" || f.style == "Bold")) || (f.family == "BIZ UDPGothic" && f.style == "Regular") || (f.family == "Noto Sans Arabic"); @@ -189,6 +189,6 @@ pub fn font(size: f32) -> egui::FontId { egui::FontId::proportional(size) } -pub fn to_color32(c: slidecraft_color::Rgba) -> Color32 { +pub fn to_color32(c: deckcraft_color::Rgba) -> Color32 { Color32::from_rgba_unmultiplied(c.r, c.g, c.b, c.a) } diff --git a/crates/ui-egui/src/thumbs.rs b/crates/ui-egui/src/thumbs.rs index 977d61c..ebdc81d 100644 --- a/crates/ui-egui/src/thumbs.rs +++ b/crates/ui-egui/src/thumbs.rs @@ -1,9 +1,9 @@ //! The slide thumbnail pane (Normal view): numbered thumbnails grouped by section, selection, //! drag-to-reorder, and the slide context menu. +use deckcraft_engine::Target; use egui::{Align2, Color32, CornerRadius, Rect, Sense, Stroke, Ui, pos2, vec2}; use serde_json::json; -use slidecraft_engine::Target; use crate::SlideApp; use crate::theme::{self, Tokens}; @@ -271,11 +271,11 @@ fn master_list(app: &mut SlideApp, ui: &mut Ui) { let img = Rect::from_min_size(pos2(row.min.x + indent, row.min.y + 4.0), vec2(tw, th)); let ppp = ui.ctx().pixels_per_point(); let px = ((tw * ppp) as u32).max(1); - let pic = slidecraft_render::render_layout( + let pic = deckcraft_render::render_layout( &doc, mi, li, - &slidecraft_render::RenderOpts { + &deckcraft_render::RenderOpts { scale: px as f64 / doc.slide_size.width.max(1.0), edit: true, size: Some((px, (px as f64 * doc.slide_size.height / doc.slide_size.width.max(1.0)) as u32)), diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index e40a8a7..47282e2 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -1,9 +1,9 @@ //! Ribbon widgets: large and small buttons, split buttons, groups, dropdowns, spinners, the colour //! picker and galleries. +use deckcraft_color::{ColorScheme, Rgba, SchemeSlot}; +use deckcraft_model::ColorRef; use egui::{Align2, Color32, CornerRadius, Rect, Response, Sense, Stroke, Ui, Vec2, pos2, vec2}; -use slidecraft_color::{ColorScheme, Rgba, SchemeSlot}; -use slidecraft_model::ColorRef; use crate::icons::{self, Icon}; use crate::theme::{self, Tokens}; @@ -287,9 +287,9 @@ pub fn color_grid(ui: &mut Ui, scheme: &ColorScheme, none_label: Option<&str>) - for row in 0..5 { for slot in slots { let base = scheme.get(slot); - let vars = slidecraft_color::theme_variants(base); + let vars = deckcraft_color::theme_variants(base); let Some((mods, label)) = vars.get(row) else { continue }; - let c = slidecraft_color::apply(base, mods); + let c = deckcraft_color::apply(base, mods); if sw(ui, c, label) { let alias = match slot { SchemeSlot::Lt1 => SchemeSlot::Bg1, @@ -310,7 +310,7 @@ pub fn color_grid(ui: &mut Ui, scheme: &ColorScheme, none_label: Option<&str>) - ui.label(egui::RichText::new("Standard Colors").font(theme::bold(12.0))); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 3.0; - for (c, name) in slidecraft_color::STANDARD_COLORS { + for (c, name) in deckcraft_color::STANDARD_COLORS { if sw(ui, c, name) { out = Some(Some(ColorRef::rgb(c))); } diff --git a/crates/vp9/Cargo.toml b/crates/vp9/Cargo.toml index e21196f..31776a6 100644 --- a/crates/vp9/Cargo.toml +++ b/crates/vp9/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "slidecraft-vp9" +name = "deckcraft-vp9" description = "Clean-room pure-Rust VP9 decoder (VP9 Bitstream & Decoding Process Specification)" version.workspace = true edition.workspace = true @@ -14,6 +14,6 @@ default = ["threads"] threads = ["dep:rayon"] [dependencies] -slidecraft-bitstream = { workspace = true } +deckcraft-bitstream = { workspace = true } thiserror = { workspace = true } rayon = { workspace = true, optional = true } diff --git a/crates/vp9/src/error.rs b/crates/vp9/src/error.rs index 660be1a..0b517cf 100644 --- a/crates/vp9/src/error.rs +++ b/crates/vp9/src/error.rs @@ -1,4 +1,4 @@ -use slidecraft_bitstream::BitError; +use deckcraft_bitstream::BitError; /// Errors produced by the decoder. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] diff --git a/crates/vp9/src/header.rs b/crates/vp9/src/header.rs index 8cbf9c3..afb119b 100644 --- a/crates/vp9/src/header.rs +++ b/crates/vp9/src/header.rs @@ -5,7 +5,7 @@ use crate::boolcoder::BoolDecoder; use crate::error::{Error, Result, ensure}; use crate::probs::FrameContext; use crate::tables::*; -use slidecraft_bitstream::BitReader; +use deckcraft_bitstream::BitReader; pub const KEY_FRAME: u8 = 0; diff --git a/crates/vp9/src/lib.rs b/crates/vp9/src/lib.rs index af15be2..3266821 100644 --- a/crates/vp9/src/lib.rs +++ b/crates/vp9/src/lib.rs @@ -10,7 +10,7 @@ //! resilient / frame parallel modes. //! //! ```no_run -//! let mut dec = slidecraft_vp9::Decoder::new(); +//! let mut dec = deckcraft_vp9::Decoder::new(); //! let chunk: Vec = Vec::new(); // one IVF / WebM frame //! for pic in dec.decode(&chunk, 0).unwrap() { //! println!("{}x{} {}-bit", pic.width, pic.height, pic.bit_depth); diff --git a/docs/brand/LICENSE-brand.txt b/docs/brand/LICENSE-brand.txt index 655ac6b..534146b 100644 --- a/docs/brand/LICENSE-brand.txt +++ b/docs/brand/LICENSE-brand.txt @@ -2,7 +2,7 @@ ArtCraft brand assets: license The ArtCraft name, wordmark and mark (the "ArtCraft Marks"), including every file in this directory in every color, size and format, are owned by the ArtCraft Team. They are trademarks -and are NOT open source. They are not covered by SlideCraft's MIT OR Apache-2.0 license, and nothing in +and are NOT open source. They are not covered by DeckCraft's MIT OR Apache-2.0 license, and nothing in that license, in NOTICE or in any other file of this repository grants any right to them. Permission @@ -11,7 +11,7 @@ The ArtCraft Team grants a limited, non-exclusive, non-transferable, revocable, permission to use the ArtCraft Marks only: 1. as part of copies of this repository; and - 2. within SlideCraft itself, its documentation and its official builds, to identify SlideCraft as made + 2. within DeckCraft itself, its documentation and its official builds, to identify DeckCraft as made by the ArtCraft team and community and to link to getartcraft.com and its community. Conditions @@ -21,17 +21,17 @@ The permission holds only while every one of these is true: - The marks are used exactly as supplied in this directory. Do not redraw, recolor, crop, stretch, rotate, animate, outline or add effects to them, extract parts of them, or combine them with any other name, mark or artwork. - - They are used only in the context of SlideCraft, and only to say that the ArtCraft team makes it. + - They are used only in the context of DeckCraft, and only to say that the ArtCraft team makes it. They must never suggest that the ArtCraft Team made, sponsors, endorses, certifies or supports any other product, service, fork, modified version, plug-in, organization or person. - Neither the marks nor the ArtCraft name appear in the name, logo, icon, domain, social media - handle, app-store listing, advertising or marketing of anything other than SlideCraft. + handle, app-store listing, advertising or marketing of anything other than DeckCraft. Forks and modified versions -If you distribute or publish a modified version of SlideCraft, or any work derived from it, you must +If you distribute or publish a modified version of DeckCraft, or any work derived from it, you must remove the ArtCraft Marks from it (or replace them with your own) and must not call it ArtCraft or -present it as an ArtCraft product. You may say, in plain text, that your work is based on SlideCraft by +present it as an ArtCraft product. You may say, in plain text, that your work is based on DeckCraft by the ArtCraft team. A fork kept only to propose changes back to this repository may keep the marks unchanged. diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 6fdbaac..51dc325 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -12,7 +12,7 @@ Coordinates are slide points (1/72 inch; a 16:9 slide is 960 × 540) unless note | Method | Params | What it does | |---|---|---| -| `engine.execute` / `ui.menu.invoke` / `command` | `{command, params}` | Run any command (see `engine.commands`; `slidecraft-cli describe ID` documents one) | +| `engine.execute` / `ui.menu.invoke` / `command` | `{command, params}` | Run any command (see `engine.commands`; `deckcraft-cli describe ID` documents one) | | `engine.commands` | — | Every command: id, label, menu path, shortcut, params doc, enabled / disabled reason | | `document.inspect` | — | Slides, sections, layouts, shapes, selection | | `ui.inspect` | — | UI state, active tool, window/canvas/slide rects, dialog, slide show state, documents, perf (fps, render ms) | @@ -28,5 +28,5 @@ Coordinates are slide points (1/72 inch; a 16:9 slide is 960 × 540) unless note | `show.start` / `show.end` | `{from?: slide index, presenter?, reading?}` | Slide show | | `app.open` / `app.save` / `app.export` / `app.quit` | `{path, …}` | Files (export takes the `file.export` params, e.g. `{"path": "deck.pdf", "layout": "handouts", "perPage": 3}`) | -The MCP server (`slidecraft-cli mcp`, see [mcp.md](mcp.md)) wraps the same methods for Claude -and other agents; `slidecraft-cli app METHOD [JSON]` sends one request from a shell. +The MCP server (`deckcraft-cli mcp`, see [mcp.md](mcp.md)) wraps the same methods for Claude +and other agents; `deckcraft-cli app METHOD [JSON]` sends one request from a shell. diff --git a/docs/mcp.md b/docs/mcp.md index b23e02a..f7e24c7 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,14 +1,14 @@ # MCP server -`slidecraft-cli mcp` runs a Model Context Protocol server on stdio. By default it drives its own -headless engine (no window); with `--connect PORT` it drives a running SlideCraft started with +`deckcraft-cli mcp` runs a Model Context Protocol server on stdio. By default it drives its own +headless engine (no window); with `--connect PORT` it drives a running DeckCraft started with `--control PORT`, so an agent and a person can work on the same deck. `--sample` opens the sample deck. ```json { "mcpServers": { - "slidecraft": { "command": "slidecraft-cli", "args": ["mcp", "--connect", "7990"] } + "deckcraft": { "command": "deckcraft-cli", "args": ["mcp", "--connect", "7990"] } } } ``` @@ -20,7 +20,7 @@ deck. | `list_commands` | Every command (id, label, menu, shortcut, params doc), optionally filtered | | `run_command` | Run one command: `{command, params}` — every menu item, ribbon button and gesture is a command | | `batch` | Run several commands in order and stop at the first error | -| `new_presentation`, `open_presentation`, `save_presentation`, `export` | Files: native `.slidecraft`, PowerPoint `.pptx`, PDF (slides, notes pages, handouts), PNG/JPEG, outline | +| `new_presentation`, `open_presentation`, `save_presentation`, `export` | Files: native `.deckcraft`, PowerPoint `.pptx`, PDF (slides, notes pages, handouts), PNG/JPEG, outline | | `inspect_document`, `inspect_slide` | Structure of the deck / one slide (shapes, text, placeholders, animations) | | `render_slide` | A slide as an image the model can look at | | `add_slide`, `go_to_slide` | Slides by layout | @@ -33,5 +33,5 @@ deck. Useful commands for building decks: `slide.new {layout, title, body}`, `shape.insert {preset, rect, text}`, `shape.connect {from, to, preset}`, `shape.freeform {points, closed, smooth}`, `shape.merge {op, ids}`, `format.*`, `design.theme`, `transition.set`, `animation.set`, -`chart.*`, `table.*`, `file.export {path, layout}`. `slidecraft-cli describe ID` prints any +`chart.*`, `table.*`, `file.export {path, layout}`. `deckcraft-cli describe ID` prints any command's parameters. diff --git a/docs/parity.md b/docs/parity.md index cae1d6a..8c3b09b 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -11,13 +11,13 @@ Each row is scored **D** (done), **P** (partial: works but lacks options, UI or Live Presentations), add-ins and VBA macros. -**Weighted breadth parity: 78%** over 187 features. +**Weighted breadth parity: 79%** over 187 features. | Priority | Features | Done | Partial | Missing | Parity | |---|---|---|---|---|---| -| P0 | 72 | 61 | 10 | 1 | 92% | -| P1 | 65 | 35 | 23 | 7 | 72% | -| P2 | 50 | 11 | 11 | 28 | 33% | +| P0 | 72 | 62 | 10 | 0 | 93% | +| P1 | 65 | 37 | 22 | 6 | 74% | +| P2 | 50 | 12 | 11 | 27 | 35% | | Area | Parity | |---|---| @@ -34,7 +34,7 @@ Live Presentations), add-ins and VBA macros. | Charts | 65% | | SmartArt | 33% | | Pictures | 58% | -| Media | 27% | +| Media | 68% | | Transitions | 83% | | Animations | 74% | | Slide show | 66% | @@ -42,9 +42,9 @@ Live Presentations), add-ins and VBA macros. | Draw | 33% | | Undo & clipboard | 88% | | Automation (✱) | 100% | -| Platforms (✱) | 25% | +| Platforms (✱) | 67% | -Open P0 items: Start screen (new, templates/themes, recent), Contextual tabs (Shape Format, Picture Format, Table Design/Layout, Chart Design), Export to PDF (slides, notes pages, handouts; text layer, links, bookmarks; vector artwork pending), Format Shape pane, Edit chart data, Crop (and crop to shape, aspect ratio), Insert audio (any common format), Audio/video playback in the show, Animation pane (reorder, timing), Presenter view (notes, next slide, timer), macOS, Windows, Linux builds. +Open P0 items: Start screen (new, templates/themes, recent), Contextual tabs (Shape Format, Picture Format, Table Design/Layout, Chart Design), Export to PDF (slides, notes pages, handouts; text layer, links, bookmarks; vector artwork pending), Format Shape pane, Edit chart data, Crop (and crop to shape, aspect ratio), Audio/video playback in the show (WMA/WMV not decodable yet), Animation pane (reorder, timing), Presenter view (notes, next slide, timer), macOS, Windows, Linux builds. ## Rows @@ -194,10 +194,10 @@ Open P0 items: Start screen (new, templates/themes, recent), Contextual tabs (Sh | Pictures | Change / reset picture | P1 | D | | Pictures | Compress pictures | P2 | M | | Pictures | Paste image from clipboard | P0 | D | -| Media | Insert audio (any common format) | P0 | P | -| Media | Insert video | P1 | P | -| Media | Audio/video playback in the show | P0 | M | -| Media | Trim, fade in/out, volume, loop, play across slides | P2 | P | +| Media | Insert audio (any common format) | P0 | D | +| Media | Insert video | P1 | D | +| Media | Audio/video playback in the show (WMA/WMV not decodable yet) | P0 | P | +| Media | Trim, fade in/out, volume, loop, play across slides | P2 | D | | Media | Record audio / screen recording | P2 | M | | Media | Icons, 3-D models, stock content | P2 | M | | Transitions | Transition gallery (fade, push, wipe, split, cover, uncover, zoom, morph…) | P0 | D | @@ -235,5 +235,5 @@ Open P0 items: Start screen (new, templates/themes, recent), Contextual tabs (Sh | Automation (✱) | Every command scriptable (CLI, control channel, MCP) | P0 | D | | Automation (✱) | Headless rendering and conversion | P0 | D | | Platforms (✱) | macOS, Windows, Linux builds | P0 | P | -| Platforms (✱) | Web (WASM) build | P1 | M | -| Platforms (✱) | BSD build | P2 | M | +| Platforms (✱) | Web (WASM) build | P1 | D | +| Platforms (✱) | BSD build | P2 | P | diff --git a/packaging/common.sh b/packaging/common.sh index 6a0c3d8..bcb3695 100644 --- a/packaging/common.sh +++ b/packaging/common.sh @@ -4,10 +4,10 @@ # # Exports: # ROOT workspace root -# VERSION [workspace.package] version from Cargo.toml (override: SLIDECRAFT_VERSION) +# VERSION [workspace.package] version from Cargo.toml (override: DECKCRAFT_VERSION) # DIST output directory for release artifacts (default: $ROOT/dist/release) -# SLIDECRAFT_BUILD_SHA git commit shown in the About dialog (crates/ui-egui/src/dialogs.rs version_line) -# SLIDECRAFT_BUILD_DATE UTC build date, YYYY-MM-DD +# DECKCRAFT_BUILD_SHA git commit shown in the About dialog (crates/ui-egui/src/dialogs.rs version_line) +# DECKCRAFT_BUILD_DATE UTC build date, YYYY-MM-DD # CARGO_TARGET_DIR cargo's target dir (default: $ROOT/target) ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -22,7 +22,7 @@ workspace_version() { ' "$ROOT/Cargo.toml" } -VERSION="${SLIDECRAFT_VERSION:-$(workspace_version)}" +VERSION="${DECKCRAFT_VERSION:-$(workspace_version)}" if [ -z "$VERSION" ]; then echo "error: could not read [workspace.package] version from $ROOT/Cargo.toml" >&2 exit 1 @@ -33,11 +33,11 @@ DIST="${DIST:-$ROOT/dist/release}" mkdir -p "$DIST" export DIST -if [ -z "${SLIDECRAFT_BUILD_SHA:-}" ]; then - SLIDECRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)" +if [ -z "${DECKCRAFT_BUILD_SHA:-}" ]; then + DECKCRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)" fi -export SLIDECRAFT_BUILD_SHA -export SLIDECRAFT_BUILD_DATE="${SLIDECRAFT_BUILD_DATE:-$(date -u +%Y-%m-%d)}" +export DECKCRAFT_BUILD_SHA +export DECKCRAFT_BUILD_DATE="${DECKCRAFT_BUILD_DATE:-$(date -u +%Y-%m-%d)}" export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" # Emit a GitHub Actions warning (plain stderr outside Actions). diff --git a/packaging/freebsd/package.sh b/packaging/freebsd/package.sh index fc6beaf..9f6caac 100755 --- a/packaging/freebsd/package.sh +++ b/packaging/freebsd/package.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Build and package SlideCraft for FreeBSD: +# Build and package DeckCraft for FreeBSD: # -# $DIST/slidecraft--freebsd-.tar.gz bin/, share/ (desktop entry, icons, AppStream) +# $DIST/deckcraft--freebsd-.tar.gz bin/, share/ (desktop entry, icons, AppStream) # # Usage: packaging/freebsd/package.sh [--skip-build] # @@ -11,10 +11,10 @@ set -euo pipefail # shellcheck source=../common.sh . "$(dirname "${BASH_SOURCE[0]}")/../common.sh" LINUX="$ROOT/packaging/linux" -APP_ID=ai.storyteller.slidecraft +APP_ID=ai.storyteller.deckcraft if [ "${1:-}" != "--skip-build" ]; then - (cd "$ROOT" && cargo build --release --locked -p slidecraft -p slidecraft-cli) + (cd "$ROOT" && cargo build --release --locked -p deckcraft -p deckcraft-cli) fi ARCH="$(uname -m)" @@ -23,22 +23,22 @@ case "$ARCH" in arm64) ARCH=aarch64 ;; esac BIN="$CARGO_TARGET_DIR/release" -NAME="slidecraft-$VERSION-freebsd-$ARCH" +NAME="deckcraft-$VERSION-freebsd-$ARCH" WORK="$CARGO_TARGET_DIR/freebsd-package" STAGE="$WORK/$NAME" rm -rf "$WORK" -mkdir -p "$STAGE/bin" "$STAGE/share/applications" "$STAGE/share/metainfo" "$STAGE/share/mime/packages" "$STAGE/share/icons" "$STAGE/share/doc/slidecraft" +mkdir -p "$STAGE/bin" "$STAGE/share/applications" "$STAGE/share/metainfo" "$STAGE/share/mime/packages" "$STAGE/share/icons" "$STAGE/share/doc/deckcraft" -install -m 755 "$BIN/slidecraft" "$BIN/slidecraft-cli" "$STAGE/bin/" -strip "$STAGE/bin/slidecraft" "$STAGE/bin/slidecraft-cli" 2>/dev/null || true +install -m 755 "$BIN/deckcraft" "$BIN/deckcraft-cli" "$STAGE/bin/" +strip "$STAGE/bin/deckcraft" "$STAGE/bin/deckcraft-cli" 2>/dev/null || true install -m 644 "$LINUX/$APP_ID.desktop" "$STAGE/share/applications/" install -m 644 "$LINUX/$APP_ID.mime.xml" "$STAGE/share/mime/packages/$APP_ID.xml" -sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$SLIDECRAFT_BUILD_DATE/g" \ +sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$DECKCRAFT_BUILD_DATE/g" \ "$LINUX/$APP_ID.metainfo.xml.in" >"$STAGE/share/metainfo/$APP_ID.metainfo.xml" cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/share/icons/" -copy_docs "$STAGE/share/doc/slidecraft" +copy_docs "$STAGE/share/doc/deckcraft" tar -C "$WORK" -czf "$DIST/$NAME.tar.gz" "$NAME" -"$STAGE/bin/slidecraft-cli" --version +"$STAGE/bin/deckcraft-cli" --version echo "wrote $DIST/$NAME.tar.gz" ls -lh "$DIST/$NAME.tar.gz" diff --git a/packaging/icons.sh b/packaging/icons.sh index c49610f..79b5ede 100755 --- a/packaging/icons.sh +++ b/packaging/icons.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # Regenerate the platform app icons from the committed PNGs in assets/app-icon/: # -# assets/app-icon/slidecraft.ico Windows (16–256 px, from hicolor/*) -# assets/app-icon/slidecraft.icns macOS (16–1024 px, from slidecraft-1024.png; needs sips + iconutil) +# assets/app-icon/deckcraft.ico Windows (16–256 px, from hicolor/*) +# assets/app-icon/deckcraft.icns macOS (16–1024 px, from deckcraft-1024.png; needs sips + iconutil) # -# The PNGs themselves are rendered by SlideCraft from assets/app-icon/icon-source.slidecraft. +# The PNGs themselves are rendered by DeckCraft from assets/app-icon/icon-source.deckcraft. # The outputs are committed, so builds and packaging never need these tools; package scripts # call this only when an output is missing. # @@ -12,8 +12,8 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DIR="$ROOT/assets/app-icon" -ID="ai.storyteller.slidecraft" -SRC="$DIR/slidecraft-1024.png" +ID="ai.storyteller.deckcraft" +SRC="$DIR/deckcraft-1024.png" WANT="${*:-ico icns}" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT @@ -24,25 +24,25 @@ case " $WANT " in for s in 16 24 32 48 64 128 256; do ICO_PNGS+=("$DIR/hicolor/${s}x${s}/apps/$ID.png") done - (cd "$ROOT" && cargo run -q -p xtask -- ico "$DIR/slidecraft.ico" "${ICO_PNGS[@]}") - echo "wrote $DIR/slidecraft.ico" + (cd "$ROOT" && cargo run -q -p xtask -- ico "$DIR/deckcraft.ico" "${ICO_PNGS[@]}") + echo "wrote $DIR/deckcraft.ico" ;; esac case " $WANT " in *" icns "*) if command -v iconutil >/dev/null && command -v sips >/dev/null; then - SET="$TMP/slidecraft.iconset" + SET="$TMP/deckcraft.iconset" mkdir -p "$SET" for s in 16 32 128 256 512; do sips -z "$s" "$s" "$SRC" --out "$SET/icon_${s}x${s}.png" >/dev/null d=$((s * 2)) sips -z "$d" "$d" "$SRC" --out "$SET/icon_${s}x${s}@2x.png" >/dev/null done - iconutil -c icns -o "$DIR/slidecraft.icns" "$SET" - echo "wrote $DIR/slidecraft.icns" + iconutil -c icns -o "$DIR/deckcraft.icns" "$SET" + echo "wrote $DIR/deckcraft.icns" else - echo "warning: sips/iconutil not found (macOS only); slidecraft.icns not regenerated" >&2 + echo "warning: sips/iconutil not found (macOS only); deckcraft.icns not regenerated" >&2 fi ;; esac diff --git a/packaging/linux/ai.storyteller.deckcraft.desktop b/packaging/linux/ai.storyteller.deckcraft.desktop new file mode 100644 index 0000000..4f6471e --- /dev/null +++ b/packaging/linux/ai.storyteller.deckcraft.desktop @@ -0,0 +1,14 @@ +[Desktop Entry] +Type=Application +Name=DeckCraft +GenericName=Presentation +Comment=Create and present slide decks +Exec=deckcraft %F +TryExec=deckcraft +Icon=ai.storyteller.deckcraft +Terminal=false +StartupNotify=true +StartupWMClass=ai.storyteller.deckcraft +Categories=Office;Presentation; +MimeType=application/x-deckcraft;application/vnd.openxmlformats-officedocument.presentationml.presentation;application/vnd.openxmlformats-officedocument.presentationml.template;application/vnd.openxmlformats-officedocument.presentationml.slideshow; +Keywords=presentation;slides;slide show;deck;pptx;keynote;powerpoint; diff --git a/packaging/linux/ai.storyteller.slidecraft.metainfo.xml.in b/packaging/linux/ai.storyteller.deckcraft.metainfo.xml.in similarity index 74% rename from packaging/linux/ai.storyteller.slidecraft.metainfo.xml.in rename to packaging/linux/ai.storyteller.deckcraft.metainfo.xml.in index 843e7db..4e6f0ae 100644 --- a/packaging/linux/ai.storyteller.slidecraft.metainfo.xml.in +++ b/packaging/linux/ai.storyteller.deckcraft.metainfo.xml.in @@ -2,17 +2,17 @@ - ai.storyteller.slidecraft + ai.storyteller.deckcraft CC0-1.0 MIT OR Apache-2.0 - SlideCraft + DeckCraft Create and present slide decks Learning Machines LLC

- SlideCraft builds presentations and slide shows: themes, layouts, shapes, pictures, + DeckCraft builds presentations and slide shows: themes, layouts, shapes, pictures, tables, charts, transitions and animations. It opens and saves PowerPoint (.pptx) files and presents them full screen with a presenter view.

@@ -20,12 +20,12 @@ Every action is also a command that the command line and AI agents (through MCP) can drive.

- ai.storyteller.slidecraft.desktop - ai.storyteller.slidecraft + ai.storyteller.deckcraft.desktop + ai.storyteller.deckcraft - https://getartcraft.com/apps/slidecraft - https://github.com/storytold/slidecraft/issues - https://github.com/storytold/slidecraft + https://getartcraft.com/apps/deckcraft + https://github.com/storytold/deckcraft/issues + https://github.com/storytold/deckcraft https://discord.gg/artcraft Office @@ -49,8 +49,8 @@ pointing - slidecraft - application/x-slidecraft + deckcraft + application/x-deckcraft diff --git a/packaging/linux/ai.storyteller.slidecraft.mime.xml b/packaging/linux/ai.storyteller.deckcraft.mime.xml similarity index 51% rename from packaging/linux/ai.storyteller.slidecraft.mime.xml rename to packaging/linux/ai.storyteller.deckcraft.mime.xml index 49530e4..64e2c01 100644 --- a/packaging/linux/ai.storyteller.slidecraft.mime.xml +++ b/packaging/linux/ai.storyteller.deckcraft.mime.xml @@ -1,9 +1,9 @@ - - SlideCraft presentation + + DeckCraft presentation - - + + diff --git a/packaging/linux/ai.storyteller.slidecraft.desktop b/packaging/linux/ai.storyteller.slidecraft.desktop deleted file mode 100644 index 099d43b..0000000 --- a/packaging/linux/ai.storyteller.slidecraft.desktop +++ /dev/null @@ -1,14 +0,0 @@ -[Desktop Entry] -Type=Application -Name=SlideCraft -GenericName=Presentation -Comment=Create and present slide decks -Exec=slidecraft %F -TryExec=slidecraft -Icon=ai.storyteller.slidecraft -Terminal=false -StartupNotify=true -StartupWMClass=ai.storyteller.slidecraft -Categories=Office;Presentation; -MimeType=application/x-slidecraft;application/vnd.openxmlformats-officedocument.presentationml.presentation;application/vnd.openxmlformats-officedocument.presentationml.template;application/vnd.openxmlformats-officedocument.presentationml.slideshow; -Keywords=presentation;slides;slide show;deck;pptx;keynote;powerpoint; diff --git a/packaging/linux/flatpak/ai.storyteller.slidecraft.yml b/packaging/linux/flatpak/ai.storyteller.deckcraft.yml similarity index 67% rename from packaging/linux/flatpak/ai.storyteller.slidecraft.yml rename to packaging/linux/flatpak/ai.storyteller.deckcraft.yml index 14dd78d..5c31015 100644 --- a/packaging/linux/flatpak/ai.storyteller.slidecraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.deckcraft.yml @@ -1,20 +1,20 @@ -# Flatpak manifest for SlideCraft, ready for a Flathub submission. +# Flatpak manifest for DeckCraft, ready for a Flathub submission. # # Flatpak builds run offline, so the crate sources must be vendored first. From the repo root: # # curl -fsSLO https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py # python3 flatpak-cargo-generator.py Cargo.lock -o packaging/linux/flatpak/cargo-sources.json -# flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.slidecraft.yml -# flatpak run ai.storyteller.slidecraft +# flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.deckcraft.yml +# flatpak run ai.storyteller.deckcraft # # For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit. -id: ai.storyteller.slidecraft +id: ai.storyteller.deckcraft runtime: org.freedesktop.Platform runtime-version: '25.08' sdk: org.freedesktop.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.rust-stable -command: slidecraft +command: deckcraft finish-args: - --share=ipc - --socket=wayland @@ -27,23 +27,23 @@ finish-args: build-options: append-path: /usr/lib/sdk/rust-stable/bin modules: - - name: slidecraft + - name: deckcraft buildsystem: simple build-options: env: - CARGO_HOME: /run/build/slidecraft/cargo + CARGO_HOME: /run/build/deckcraft/cargo CARGO_NET_OFFLINE: 'true' build-commands: - - cargo --offline build --release --locked -p slidecraft - - install -Dm755 target/release/slidecraft /app/bin/slidecraft - - install -Dm644 packaging/linux/ai.storyteller.slidecraft.desktop /app/share/applications/ai.storyteller.slidecraft.desktop - - install -Dm644 packaging/linux/ai.storyteller.slidecraft.mime.xml /app/share/mime/packages/ai.storyteller.slidecraft.xml + - cargo --offline build --release --locked -p deckcraft + - install -Dm755 target/release/deckcraft /app/bin/deckcraft + - install -Dm644 packaging/linux/ai.storyteller.deckcraft.desktop /app/share/applications/ai.storyteller.deckcraft.desktop + - install -Dm644 packaging/linux/ai.storyteller.deckcraft.mime.xml /app/share/mime/packages/ai.storyteller.deckcraft.xml - mkdir -p /app/share/icons && cp -R assets/app-icon/hicolor /app/share/icons/ - >- version=$(awk '/^\[/{p=($0=="[workspace.package]");next} p&&$1=="version"{gsub(/[" ]/,"",$3);print $3;exit}' Cargo.toml); install -d /app/share/metainfo; sed -e "s/@VERSION@/$version/" -e "s/@DATE@/$(date -u +%Y-%m-%d)/" - packaging/linux/ai.storyteller.slidecraft.metainfo.xml.in > /app/share/metainfo/ai.storyteller.slidecraft.metainfo.xml + packaging/linux/ai.storyteller.deckcraft.metainfo.xml.in > /app/share/metainfo/ai.storyteller.deckcraft.metainfo.xml sources: - type: dir path: ../../.. diff --git a/packaging/linux/nfpm.yaml b/packaging/linux/nfpm.yaml index 5e04c86..6542a35 100644 --- a/packaging/linux/nfpm.yaml +++ b/packaging/linux/nfpm.yaml @@ -3,7 +3,7 @@ # exports VERSION and NFPM_ARCH. # # nfpm package -f packaging/linux/nfpm.yaml -p deb -t dist/release/ -name: slidecraft +name: deckcraft arch: ${NFPM_ARCH} platform: linux version: ${VERSION} @@ -12,16 +12,16 @@ version_schema: semver release: 1 section: editors priority: optional -maintainer: ${SLIDECRAFT_MAINTAINER} +maintainer: ${DECKCRAFT_MAINTAINER} vendor: Learning Machines LLC -homepage: https://getartcraft.com/apps/slidecraft +homepage: https://getartcraft.com/apps/deckcraft license: MIT OR Apache-2.0 description: |- Open-source, native presentation app that works the way PowerPoint users expect. Themes, layouts, shapes, pictures, tables, charts, transitions and animations; opens and saves PowerPoint (.pptx) files; full-screen slide show with presenter view; GPU rendering (wgpu). - Includes slidecraft-cli, a headless renderer and command runner. + Includes deckcraft-cli, a headless renderer and command runner. # The binaries link only glibc and libgcc_s directly. Windowing (X11/Wayland, xkbcommon) and the # GPU (Vulkan, EGL/GL) are loaded at runtime, so they are dependencies/recommendations here @@ -56,11 +56,11 @@ overrides: - xdg-desktop-portal contents: - - src: ${STAGE}/usr/bin/slidecraft - dst: /usr/bin/slidecraft + - src: ${STAGE}/usr/bin/deckcraft + dst: /usr/bin/deckcraft file_info: { mode: 0755 } - - src: ${STAGE}/usr/bin/slidecraft-cli - dst: /usr/bin/slidecraft-cli + - src: ${STAGE}/usr/bin/deckcraft-cli + dst: /usr/bin/deckcraft-cli file_info: { mode: 0755 } - src: ${STAGE}/usr/share/ dst: /usr/share/ diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index d0560d1..721e0d7 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# Build and package SlideCraft for Linux ( is x86_64 or aarch64): +# Build and package DeckCraft for Linux ( is x86_64 or aarch64): # -# $DIST/slidecraft--linux-.AppImage any distro with glibc >= the build host's -# $DIST/slidecraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... -# $DIST/slidecraft--linux-.rpm Fedora, openSUSE, RHEL, ... -# $DIST/slidecraft--linux-.tar.gz plain FHS-style tree (bin/, share/) +# $DIST/deckcraft--linux-.AppImage any distro with glibc >= the build host's +# $DIST/deckcraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... +# $DIST/deckcraft--linux-.rpm Fedora, openSUSE, RHEL, ... +# $DIST/deckcraft--linux-.tar.gz plain FHS-style tree (bin/, share/) # # Usage: packaging/linux/package.sh [--skip-build] [--formats "appimage deb rpm tar"] # @@ -15,7 +15,7 @@ set -euo pipefail # shellcheck source=../common.sh . "$(dirname "${BASH_SOURCE[0]}")/../common.sh" HERE="$ROOT/packaging/linux" -APP_ID=ai.storyteller.slidecraft +APP_ID=ai.storyteller.deckcraft SKIP_BUILD=0 FORMATS="appimage deb rpm tar" @@ -34,13 +34,13 @@ case "$ARCH" in aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;; *) echo "unsupported architecture $ARCH" >&2; exit 2 ;; esac -export SLIDECRAFT_MAINTAINER="${SLIDECRAFT_MAINTAINER:-SlideCraft maintainers }" -BASENAME="slidecraft-$VERSION-linux-$ARCH" +export DECKCRAFT_MAINTAINER="${DECKCRAFT_MAINTAINER:-DeckCraft maintainers }" +BASENAME="deckcraft-$VERSION-linux-$ARCH" -echo "==> SlideCraft $VERSION for Linux $ARCH ($FORMATS)" +echo "==> DeckCraft $VERSION for Linux $ARCH ($FORMATS)" if [ "$SKIP_BUILD" = 0 ]; then - (cd "$ROOT" && cargo build --release --locked -p slidecraft -p slidecraft-cli) + (cd "$ROOT" && cargo build --release --locked -p deckcraft -p deckcraft-cli) fi BIN="$CARGO_TARGET_DIR/release" WORK="$CARGO_TARGET_DIR/linux-package" @@ -48,18 +48,18 @@ STAGE="$WORK/root" rm -rf "$WORK" # ---- stage an FHS tree (shared by every format) ------------------------------------------------- -install -Dm755 "$BIN/slidecraft" "$STAGE/usr/bin/slidecraft" -install -Dm755 "$BIN/slidecraft-cli" "$STAGE/usr/bin/slidecraft-cli" -strip "$STAGE/usr/bin/slidecraft" "$STAGE/usr/bin/slidecraft-cli" 2>/dev/null || true +install -Dm755 "$BIN/deckcraft" "$STAGE/usr/bin/deckcraft" +install -Dm755 "$BIN/deckcraft-cli" "$STAGE/usr/bin/deckcraft-cli" +strip "$STAGE/usr/bin/deckcraft" "$STAGE/usr/bin/deckcraft-cli" 2>/dev/null || true install -Dm644 "$HERE/$APP_ID.desktop" "$STAGE/usr/share/applications/$APP_ID.desktop" install -Dm644 "$HERE/$APP_ID.mime.xml" "$STAGE/usr/share/mime/packages/$APP_ID.xml" mkdir -p "$STAGE/usr/share/metainfo" -sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$SLIDECRAFT_BUILD_DATE/g" \ +sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$DECKCRAFT_BUILD_DATE/g" \ "$HERE/$APP_ID.metainfo.xml.in" >"$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml" mkdir -p "$STAGE/usr/share/icons" cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/usr/share/icons/" -mkdir -p "$STAGE/usr/share/doc/slidecraft" -copy_docs "$STAGE/usr/share/doc/slidecraft" +mkdir -p "$STAGE/usr/share/doc/deckcraft" +copy_docs "$STAGE/usr/share/doc/deckcraft" if command -v desktop-file-validate >/dev/null; then desktop-file-validate "$STAGE/usr/share/applications/$APP_ID.desktop" @@ -92,10 +92,10 @@ fi # ---- AppImage ----------------------------------------------------------------------------------- if has appimage; then - APPDIR="$WORK/SlideCraft.AppDir" + APPDIR="$WORK/DeckCraft.AppDir" cp -R "$STAGE" "$APPDIR" mv "$APPDIR/usr/share/doc" "$WORK/doc-unused" - ln -s usr/bin/slidecraft "$APPDIR/AppRun" + ln -s usr/bin/deckcraft "$APPDIR/AppRun" cp "$HERE/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop" cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png" ln -s "$APP_ID.png" "$APPDIR/.DirIcon" @@ -115,6 +115,6 @@ if has appimage; then echo "wrote $OUT" fi -"$STAGE/usr/bin/slidecraft-cli" --version +"$STAGE/usr/bin/deckcraft-cli" --version echo "==> done" ls -lh "$DIST" diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in index 347fe72..22afe13 100644 --- a/packaging/macos/Info.plist.in +++ b/packaging/macos/Info.plist.in @@ -7,13 +7,13 @@ CFBundleDevelopmentRegion en CFBundleDisplayName - SlideCraft + DeckCraft CFBundleName - SlideCraft + DeckCraft CFBundleExecutable - SlideCraft + DeckCraft CFBundleIdentifier - ai.storyteller.slidecraft + ai.storyteller.deckcraft CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType @@ -24,12 +24,12 @@ @SHORT_VERSION@ CFBundleVersion @SHORT_VERSION@ - SlideCraftVersion + DeckCraftVersion @VERSION@ - SlideCraftBuildCommit + DeckCraftBuildCommit @BUILD_SHA@ CFBundleIconFile - SlideCraft + DeckCraft LSApplicationCategoryType public.app-category.productivity + UTExportedTypeDeclarations UTTypeIdentifier - ai.storyteller.slidecraft.presentation + ai.storyteller.deckcraft.presentation UTTypeDescription - SlideCraft Presentation + DeckCraft Presentation UTTypeConformsTo public.data public.presentation UTTypeIconFile - SlideCraft + DeckCraft UTTypeTagSpecification public.filename-extension - slidecraft + deckcraft public.mime-type - application/x-slidecraft + application/x-deckcraft diff --git a/packaging/macos/entitlements.plist b/packaging/macos/entitlements.plist index e3c22ff..57da675 100644 --- a/packaging/macos/entitlements.plist +++ b/packaging/macos/entitlements.plist @@ -1,6 +1,6 @@ - diff --git a/packaging/macos/import-cert.sh b/packaging/macos/import-cert.sh index 5be3ebf..f27d322 100755 --- a/packaging/macos/import-cert.sh +++ b/packaging/macos/import-cert.sh @@ -18,8 +18,8 @@ if [ -z "${APPLE_CERTIFICATE:-}" ]; then fi TMP="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" -KEYCHAIN="$TMP/slidecraft-signing.keychain-db" -CERT="$TMP/slidecraft-signing.p12" +KEYCHAIN="$TMP/deckcraft-signing.keychain-db" +CERT="$TMP/deckcraft-signing.p12" KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}" trap 'rm -f "$CERT"' EXIT diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index b7da66a..26fdbe5 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Build, sign and (optionally) notarize the macOS release artifacts: # -# $DIST/slidecraft--macos-.dmg SlideCraft.app on a drag-to-Applications DMG -# $DIST/slidecraft-cli--macos-.zip the headless CLI +# $DIST/deckcraft--macos-.dmg DeckCraft.app on a drag-to-Applications DMG +# $DIST/deckcraft-cli--macos-.zip the headless CLI # # Usage: packaging/macos/package.sh [--arch universal|aarch64|x86_64] [--skip-build] # @@ -39,9 +39,9 @@ export MACOSX_DEPLOYMENT_TARGET=11.0 IDENTITY="${MACOS_SIGN_IDENTITY:--}" SHORT_VERSION="${VERSION%%-*}" WORK="$CARGO_TARGET_DIR/macos-package" -APP="$WORK/SlideCraft.app" -DMG="$DIST/slidecraft-$VERSION-macos-$ARCH.dmg" -CLI_ZIP="$DIST/slidecraft-cli-$VERSION-macos-$ARCH.zip" +APP="$WORK/DeckCraft.app" +DMG="$DIST/deckcraft-$VERSION-macos-$ARCH.dmg" +CLI_ZIP="$DIST/deckcraft-cli-$VERSION-macos-$ARCH.zip" NOTARIZE=0 if [ "$IDENTITY" = "-" ]; then @@ -52,18 +52,18 @@ else warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized" fi -echo "==> SlideCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE" +echo "==> DeckCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE" # ---- build ------------------------------------------------------------------------------------- if [ "$SKIP_BUILD" = 0 ]; then args=() for t in "${TARGETS[@]}"; do args+=(--target "$t"); done - (cd "$ROOT" && cargo build --release --locked -p slidecraft -p slidecraft-cli "${args[@]}") + (cd "$ROOT" && cargo build --release --locked -p deckcraft -p deckcraft-cli "${args[@]}") fi rm -rf "$WORK" mkdir -p "$WORK/bin" -for bin in slidecraft slidecraft-cli; do +for bin in deckcraft deckcraft-cli; do inputs=() for t in "${TARGETS[@]}"; do inputs+=("$CARGO_TARGET_DIR/$t/release/$bin"); done lipo -create -output "$WORK/bin/$bin" "${inputs[@]}" @@ -97,29 +97,29 @@ notarize() { fi } -# ---- SlideCraft.app ---------------------------------------------------------------------------- +# ---- DeckCraft.app ---------------------------------------------------------------------------- echo "==> assembling $APP" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" # Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile). -cp "$WORK/bin/slidecraft" "$APP/Contents/MacOS/SlideCraft" -cp "$ROOT/assets/app-icon/slidecraft.icns" "$APP/Contents/Resources/SlideCraft.icns" +cp "$WORK/bin/deckcraft" "$APP/Contents/MacOS/DeckCraft" +cp "$ROOT/assets/app-icon/deckcraft.icns" "$APP/Contents/Resources/DeckCraft.icns" # Licences of the embedded craft-fonts fonts (only when built with CRAFT_FONTS_DIR). copy_font_licences "$APP/Contents/Resources" sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \ - -e "s/@BUILD_SHA@/${SLIDECRAFT_BUILD_SHA:-unknown}/g" \ + -e "s/@BUILD_SHA@/${DECKCRAFT_BUILD_SHA:-unknown}/g" \ "$HERE/Info.plist.in" >"$APP/Contents/Info.plist" plutil -lint "$APP/Contents/Info.plist" printf 'APPL????' >"$APP/Contents/PkgInfo" # Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature). # Today the only nested code is the main executable; frameworks/helpers would be signed here too. -sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/SlideCraft" +sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/DeckCraft" sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP" codesign --verify --strict --deep --verbose=2 "$APP" if [ "$NOTARIZE" = 1 ]; then - ditto -c -k --keepParent "$APP" "$WORK/SlideCraft-notarize.zip" - notarize "$WORK/SlideCraft-notarize.zip" + ditto -c -k --keepParent "$APP" "$WORK/DeckCraft-notarize.zip" + notarize "$WORK/DeckCraft-notarize.zip" xcrun stapler staple "$APP" xcrun stapler validate "$APP" spctl --assess --type execute -vvv "$APP" @@ -129,12 +129,12 @@ fi echo "==> building $DMG" STAGE="$WORK/dmg" mkdir -p "$STAGE" -ditto "$APP" "$STAGE/SlideCraft.app" +ditto "$APP" "$STAGE/DeckCraft.app" ln -s /Applications "$STAGE/Applications" rm -f "$DMG" "$WORK/raw.dmg" # makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, # which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions. -hdiutil makehybrid -hfs -hfs-volume-name "SlideCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" +hdiutil makehybrid -hfs -hfs-volume-name "DeckCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" rm -f "$WORK/raw.dmg" sign "$DMG" @@ -148,17 +148,17 @@ fi # ---- CLI --------------------------------------------------------------------------------------- echo "==> building $CLI_ZIP" -CLI_DIR="$WORK/slidecraft-cli-$VERSION-macos-$ARCH" +CLI_DIR="$WORK/deckcraft-cli-$VERSION-macos-$ARCH" mkdir -p "$CLI_DIR" -cp "$WORK/bin/slidecraft-cli" "$CLI_DIR/" +cp "$WORK/bin/deckcraft-cli" "$CLI_DIR/" copy_docs "$CLI_DIR" -sign --options runtime "$CLI_DIR/slidecraft-cli" -codesign --verify --strict --verbose=2 "$CLI_DIR/slidecraft-cli" +sign --options runtime "$CLI_DIR/deckcraft-cli" +codesign --verify --strict --verbose=2 "$CLI_DIR/deckcraft-cli" rm -f "$CLI_ZIP" ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP" # A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online. if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi -"$WORK/bin/slidecraft-cli" --version +"$WORK/bin/deckcraft-cli" --version echo "==> done" ls -lh "$DMG" "$CLI_ZIP" diff --git a/packaging/web/README.md b/packaging/web/README.md index 4b272bf..3b4f9a2 100644 --- a/packaging/web/README.md +++ b/packaging/web/README.md @@ -1,22 +1,22 @@ -# Hosting SlideCraft for the web +# Hosting DeckCraft for the web -`slidecraft-web-.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a -static site in `slidecraft-web-/`: +`deckcraft-web-.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a +static site in `deckcraft-web-/`: | File | What it is | |---|---| | `index.html` | The page. It loads everything through relative URLs. | -| `slidecraft-web-.js` | wasm-bindgen glue (generated, ES module) | -| `slidecraft-web-_bg.wasm` | The app, size is printed by `packaging/web/package.sh` | +| `deckcraft-web-.js` | wasm-bindgen glue (generated, ES module) | +| `deckcraft-web-_bg.wasm` | The app, size is printed by `packaging/web/package.sh` | | `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache | There is no server-side code. Upload the folder's contents anywhere that serves static files. ## Any path works -All URLs in `index.html` are relative (`public_url = "./"` in `apps/slidecraft-web/Trunk.toml`), +All URLs in `index.html` are relative (`public_url = "./"` in `apps/deckcraft-web/Trunk.toml`), so the site works at a domain root (`https://example.com/`), under a prefix -(`https://example.com/tools/slidecraft/`) and from a CDN bucket. The asset names carry a content +(`https://example.com/tools/deckcraft/`) and from a CDN bucket. The asset names carry a content hash, so they can be cached forever. Only `index.html` needs revalidation. ## Required server settings @@ -31,7 +31,7 @@ hash, so they can be cached forever. Only `index.html` needs revalidation. `.js` files, and `no-cache` on `index.html`. - **HTTPS:** WebGPU (and the clipboard) only work in a secure context, which means `https://` or `http://localhost`. Over plain HTTP elsewhere, the app falls back to WebGL2. -- **No special isolation headers:** SlideCraft doesn't use `SharedArrayBuffer`, so it doesn't +- **No special isolation headers:** DeckCraft doesn't use `SharedArrayBuffer`, so it doesn't need `Cross-Origin-Opener-Policy` or `Cross-Origin-Embedder-Policy`. If your site already sends COEP `require-corp`, also send `Cross-Origin-Resource-Policy: same-origin` (or `cross-origin` when the files live on a CDN) on the app's files. @@ -39,7 +39,7 @@ hash, so they can be cached forever. Only `index.html` needs revalidation. nginx example: ```nginx -location /slidecraft/ { +location /deckcraft/ { types { application/wasm wasm; text/javascript js; text/html html; } gzip on; gzip_types application/wasm text/javascript text/html; @@ -54,8 +54,8 @@ Local test: `python3 -m http.server 8765` inside the folder, then open http://lo ```html