Rename SlideCraft to DeckCraft: crates, apps, binaries, app id, file extension (.deckcraft; .slidecraft files still open), packaging, docs; ROADMAP alpha and parity estimates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-06 01:46:40 -04:00
parent 49278565ea
commit ae9d525bce
216 changed files with 2021 additions and 1998 deletions

View File

@ -46,12 +46,12 @@ jobs:
} }
- name: XML, desktop entry, AppStream, Flatpak manifest - name: XML, desktop entry, AppStream, Flatpak manifest
run: | 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. # Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon.
if grep -oE '<Icon Id="[^"]*"' packaging/windows/slidecraft.wxs | grep -vE '\.(ico|exe)"$'; then if grep -oE '<Icon Id="[^"]*"' packaging/windows/deckcraft.wxs | grep -vE '\.(ico|exe)"$'; then
echo "::error::Icon Id must end in .ico or .exe"; exit 1 echo "::error::Icon Id must end in .ico or .exe"; exit 1
fi fi
desktop-file-validate packaging/linux/ai.storyteller.slidecraft.desktop 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.slidecraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.slidecraft.metainfo.xml" 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.slidecraft.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.slidecraft.yml')); assert m['id'] == 'ai.storyteller.slidecraft' and m['command'] == 'slidecraft'" 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'"

View File

@ -1,5 +1,5 @@
# Release pipeline: every push to `release` builds installers for every platform and creates or # Release pipeline: every push to `release` builds installers for every platform and creates or
# updates the draft GitHub Release "SlideCraft v<version>" (tag v<version>). Recipe: # updates the draft GitHub Release "DeckCraft v<version>" (tag v<version>). Recipe:
# craftrules release/playbook.md. # craftrules release/playbook.md.
# #
# The version has one source: `[workspace.package] version` in Cargo.toml (`cargo xtask version`, # The version has one source: `[workspace.package] version` in Cargo.toml (`cargo xtask version`,
@ -33,7 +33,7 @@ env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1 RUST_BACKTRACE: 1
SLIDECRAFT_BUILD_SHA: ${{ github.sha }} DECKCRAFT_BUILD_SHA: ${{ github.sha }}
INPUT_VERSION: ${{ inputs.version }} INPUT_VERSION: ${{ inputs.version }}
# Optional font input (storytold/craft-fonts docs/integration.md); releases always embed it. # Optional font input (storytold/craft-fonts docs/integration.md); releases always embed it.
# This commit adds fonts/latin-manifest.txt (the Latin presentation fonts). # This commit adds fonts/latin-manifest.txt (the Latin presentation fonts).
@ -66,7 +66,7 @@ jobs:
echo "date=$(date -u +%Y-%m-%d)" echo "date=$(date -u +%Y-%m-%d)"
echo "prerelease=$pre" echo "prerelease=$pre"
} >>"$GITHUB_OUTPUT" } >>"$GITHUB_OUTPUT"
echo "### SlideCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" echo "### DeckCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
macos: macos:
name: macOS universal name: macOS universal
@ -74,8 +74,8 @@ jobs:
runs-on: macos-15 runs-on: macos-15
environment: release environment: release
env: env:
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Check out craft-fonts - name: Check out craft-fonts
@ -92,7 +92,7 @@ jobs:
key: release-macos-universal key: release-macos-universal
- name: Apply version override - name: Apply version override
if: inputs.version != '' if: inputs.version != ''
run: cargo xtask version set "$SLIDECRAFT_VERSION" run: cargo xtask version set "$DECKCRAFT_VERSION"
- name: Import signing certificate - name: Import signing certificate
env: env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@ -129,8 +129,8 @@ jobs:
- { arch: x64, target: x86_64-pc-windows-msvc } - { arch: x64, target: x86_64-pc-windows-msvc }
- { arch: x86, target: i686-pc-windows-msvc } - { arch: x86, target: i686-pc-windows-msvc }
env: env:
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Check out craft-fonts - name: Check out craft-fonts
@ -148,7 +148,7 @@ jobs:
- name: Apply version override - name: Apply version override
if: inputs.version != '' if: inputs.version != ''
shell: bash shell: bash
run: cargo xtask version set "$SLIDECRAFT_VERSION" run: cargo xtask version set "$DECKCRAFT_VERSION"
- name: Install WiX - name: Install WiX
shell: pwsh shell: pwsh
run: dotnet tool install --global wix --version 5.0.2 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: x86_64, runner: ubuntu-22.04, deb: amd64 }
- { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 } - { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 }
env: env:
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
NFPM_VERSION: 2.47.0 NFPM_VERSION: 2.47.0
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -212,7 +212,7 @@ jobs:
key: release-linux-${{ matrix.arch }} key: release-linux-${{ matrix.arch }}
- name: Apply version override - name: Apply version override
if: inputs.version != '' 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) - name: Build and package (AppImage, deb, rpm, tar.gz)
env: env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
@ -221,8 +221,8 @@ jobs:
- name: Check the packages - name: Check the packages
run: | run: |
dpkg-deb --info dist/release/*.deb dpkg-deb --info dist/release/*.deb
dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/slidecraft$|metainfo|applications' dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/deckcraft$|metainfo|applications'
ldd target/release/slidecraft ldd target/release/deckcraft
for appimage in dist/release/*.AppImage; do for appimage in dist/release/*.AppImage; do
chmod +x "$appimage" chmod +x "$appimage"
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version
@ -244,14 +244,14 @@ jobs:
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08 image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
options: --privileged options: --privileged
env: env:
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Apply version override - name: Apply version override
if: inputs.version != '' if: inputs.version != ''
run: | run: |
# No cargo in this container: the same edit `cargo xtask version set` makes. # 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 grep -m1 '^version' Cargo.toml
- name: Vendor the crate sources (flatpak builds run offline) - name: Vendor the crate sources (flatpak builds run offline)
run: | run: |
@ -262,14 +262,14 @@ jobs:
python3 "$RUNNER_TEMP/flatpak-cargo-generator.py" Cargo.lock -o packaging/linux/flatpak/cargo-sources.json python3 "$RUNNER_TEMP/flatpak-cargo-generator.py" Cargo.lock -o packaging/linux/flatpak/cargo-sources.json
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6 - uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with: with:
manifest-path: packaging/linux/flatpak/ai.storyteller.slidecraft.yml manifest-path: packaging/linux/flatpak/ai.storyteller.deckcraft.yml
bundle: slidecraft-${{ needs.version.outputs.version }}-linux-x86_64.flatpak bundle: deckcraft-${{ needs.version.outputs.version }}-linux-x86_64.flatpak
upload-artifact: false upload-artifact: false
cache-key: flatpak-builder-${{ github.sha }} cache-key: flatpak-builder-${{ github.sha }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: release-flatpak name: release-flatpak
path: slidecraft-*.flatpak path: deckcraft-*.flatpak
if-no-files-found: error if-no-files-found: error
freebsd: freebsd:
@ -279,8 +279,8 @@ jobs:
environment: release environment: release
timeout-minutes: 240 timeout-minutes: 240
env: env:
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
VERSION_OVERRIDE: ${{ inputs.version }} VERSION_OVERRIDE: ${{ inputs.version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -297,7 +297,7 @@ jobs:
usesh: true usesh: true
cpu: 4 cpu: 4
mem: 12288 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 # Copies the workspace (with dist/release) back; the target dir lives outside it so the
# copy stays small. # copy stays small.
copyback: true copyback: true
@ -308,7 +308,7 @@ jobs:
. "$HOME/.cargo/env" . "$HOME/.cargo/env"
git config --global --add safe.directory "*" git config --global --add safe.directory "*"
export CARGO_TARGET_DIR="$HOME/target" CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1 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 bash packaging/freebsd/package.sh
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
@ -322,8 +322,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: release environment: release
env: env:
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }} DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
TRUNK_VERSION: 0.21.14 TRUNK_VERSION: 0.21.14
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -346,7 +346,7 @@ jobs:
trunk --version trunk --version
- name: Apply version override - name: Apply version override
if: inputs.version != '' if: inputs.version != ''
run: cargo xtask version set "$SLIDECRAFT_VERSION" run: cargo xtask version set "$DECKCRAFT_VERSION"
- name: Build and zip - name: Build and zip
env: env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
@ -389,7 +389,7 @@ jobs:
working-directory: artifacts working-directory: artifacts
run: | run: |
tag="v$VERSION" tag="v$VERSION"
title="SlideCraft v$VERSION" title="DeckCraft v$VERSION"
pre=() pre=()
if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi
if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then

View File

@ -1,6 +1,6 @@
# AGENTS.md — rules for every agent and contributor # 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, 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: Windows, Linux and FreeBSD, and on the web via WASM. Siblings with the same conventions:
PhotoCraft, VectorCraft, FilmCraft, LightCraft, PrintCraft, EffectCraft, DesignCraft 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, product" includes PowerPoint, Office, Windows and the fonts they install (Aptos, Calibri,
Cambria, Segoe, Consolas, Wingdings…). Cambria, Segoe, Consolas, Wingdings…).
- This holds even when the vendor publishes the material under an open licence, if it is **visual - 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. 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 - **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, 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 for personal use" material, screenshots of other people's software, anything whose licence you
cannot prove from its original source. 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. allowed: our UI, our/Lucide icons, OFL fonts, and decks built only from allowed assets.
### 1.3 Every asset is attributed ### 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. or `docs/brand/` has no row.
- **`NOTICE`** keeps the notices the licences require. - **`NOTICE`** keeps the notices the licences require.
- Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this - 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 `fonts/manifest.txt` when built with `CRAFT_FONTS_DIR` (local dev: `.cargo/config.toml` points at
`../craft-fonts`). `../craft-fonts`).
@ -98,7 +98,7 @@ once and tell the user.
## 3. Never crash ## 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`. Standard: craftrules `standards/never-crash.md`.
- No `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!` in non-test - No `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!` in non-test
code; no `unsafe` (`unsafe_code = "forbid"`). Every production crate root carries 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) | | L1 | `model` (presentation, masters, layouts, slides, shapes, text, themes, inheritance, text edit ops) |
| L2 | `fonts`, `text` (layout), | | L2 | `fonts`, `text` (layout), |
| L3 | `render` (vello_cpu) | | L3 | `render` (vello_cpu) |
| L4 | `format` (.slidecraft), `pptx` | | L4 | `format` (.deckcraft), `pptx` |
| L5 | `engine` (session, history, commands, tools, keys, sample) | | L5 | `engine` (session, history, commands, tools, keys, sample) |
| L6 | `mcp` | | L6 | `mcp` |
| L7 | `ui-egui` (swappable; nothing below depends on egui/eframe/winit/rfd) | | 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, - **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 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 ## 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. - 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]}}}` `{"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: then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods:
`crates/ui-egui/src/control.rs`, docs: `docs/control-protocol.md`. `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 - **For UI work, look at the result** (screenshot, read the PNG) and compare with the observations
in `plan/powerpoint/01-observed-ui.md`. in `plan/powerpoint/01-observed-ui.md`.
- Headless: `slidecraft-cli render --sample --all --scale 1 out/`, - Headless: `deckcraft-cli render --sample --all --scale 1 out/`,
`slidecraft-cli run --sample --cmd 'slide.new={"title":"Hi"}' --export out.png`. `deckcraft-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). - MCP: `deckcraft-cli mcp` (headless) or `deckcraft-cli mcp --connect 7979` (drive the app).
See `docs/mcp.md`. See `docs/mcp.md`.
- Shell gotcha on the dev machine: `mv`/`cp` are aliased interactive; use `/bin/mv -f`, `/bin/cp -f`. - 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. - Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own.

View File

@ -1,12 +1,12 @@
# Attribution # 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` 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. 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 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 ## 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.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/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 | | `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/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/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/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.slidecraft` | SlideCraft contributors | made with `slidecraft-cli run` (see git history) | MIT OR Apache-2.0 | Source of the placeholder icon | | `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`) | 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/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`) | 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/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` | SlideCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures | | `crates/media/tests/fixtures/make.sh` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures |
## Bundled through dependencies ## Bundled through dependencies

View File

@ -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 Read [AGENTS.md](AGENTS.md) first: its rules (asset policy, clean room, never crash) bind you and
win over anything here. win over anything here.
@ -10,8 +10,8 @@ win over anything here.
## Habits ## Habits
- Every user-visible action is a command with tests (`crates/engine/src/cmd/*`). - Every user-visible action is a command with tests (`crates/engine/src/cmd/*`).
- Look at UI changes: `slidecraft --sample --control 7979` + `ui.screenshot`, or - Look at UI changes: `deckcraft --sample --control 7979` + `ui.screenshot`, or
`slidecraft-cli render`. `deckcraft-cli render`.
- Commit after each landed arc (`M<n>.<k>: what`) and push to `origin main`. - Commit after each landed arc (`M<n>.<k>: what`) and push to `origin main`.
- Keep `ROADMAP.md` and `plan/STATUS.md` current. - Keep `ROADMAP.md` and `plan/STATUS.md` current.
- Don't ask the user questions; record genuinely-theirs decisions in `plan/STATUS.md`. - Don't ask the user questions; record genuinely-theirs decisions in `plan/STATUS.md`.

592
Cargo.lock generated
View File

@ -864,6 +864,302 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 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]] [[package]]
name = "derive_arbitrary" name = "derive_arbitrary"
version = "1.5.0" version = "1.5.0"
@ -3447,302 +3743,6 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 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]] [[package]]
name = "slotmap" name = "slotmap"
version = "1.1.1" version = "1.1.1"

View File

@ -1,41 +1,41 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/*", "apps/*", "xtask"] members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/slidecraft", "apps/slidecraft-cli"] default-members = ["crates/*", "apps/deckcraft", "apps/deckcraft-cli"]
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
rust-version = "1.90" rust-version = "1.90"
repository = "https://github.com/storytold/slidecraft" repository = "https://github.com/storytold/deckcraft"
[workspace.dependencies] [workspace.dependencies]
# internal # internal
slidecraft-geom = { path = "crates/geom" } deckcraft-geom = { path = "crates/geom" }
slidecraft-color = { path = "crates/color" } deckcraft-color = { path = "crates/color" }
slidecraft-model = { path = "crates/model" } deckcraft-model = { path = "crates/model" }
slidecraft-fonts = { path = "crates/fonts" } deckcraft-fonts = { path = "crates/fonts" }
slidecraft-text = { path = "crates/text" } deckcraft-text = { path = "crates/text" }
slidecraft-render = { path = "crates/render" } deckcraft-render = { path = "crates/render" }
slidecraft-pdf = { path = "crates/pdf" } deckcraft-pdf = { path = "crates/pdf" }
slidecraft-pptx = { path = "crates/pptx" } deckcraft-pptx = { path = "crates/pptx" }
slidecraft-format = { path = "crates/format" } deckcraft-format = { path = "crates/format" }
slidecraft-anim = { path = "crates/anim" } deckcraft-anim = { path = "crates/anim" }
slidecraft-engine = { path = "crates/engine" } deckcraft-engine = { path = "crates/engine" }
slidecraft-mcp = { path = "crates/mcp" } deckcraft-mcp = { path = "crates/mcp" }
slidecraft-ui-egui = { path = "crates/ui-egui" } deckcraft-ui-egui = { path = "crates/ui-egui" }
slidecraft-media = { path = "crates/media" } deckcraft-media = { path = "crates/media" }
# codecs (ported from FilmCraft, same owner and licence) # codecs (ported from FilmCraft, same owner and licence)
slidecraft-bitstream = { path = "crates/bitstream" } deckcraft-bitstream = { path = "crates/bitstream" }
slidecraft-h264 = { path = "crates/h264" } deckcraft-h264 = { path = "crates/h264" }
slidecraft-hevc = { path = "crates/hevc" } deckcraft-hevc = { path = "crates/hevc" }
slidecraft-vp9 = { path = "crates/vp9" } deckcraft-vp9 = { path = "crates/vp9" }
slidecraft-av1 = { path = "crates/av1" } deckcraft-av1 = { path = "crates/av1" }
slidecraft-isobmff = { path = "crates/isobmff" } deckcraft-isobmff = { path = "crates/isobmff" }
slidecraft-matroska = { path = "crates/matroska" } deckcraft-matroska = { path = "crates/matroska" }
slidecraft-ogg = { path = "crates/ogg" } deckcraft-ogg = { path = "crates/ogg" }
slidecraft-opus = { path = "crates/opus" } deckcraft-opus = { path = "crates/opus" }
# foundation # foundation
kurbo = { version = "0.13", features = ["serde"] } kurbo = { version = "0.13", features = ["serde"] }

View File

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

4
NOTICE
View File

@ -1,5 +1,5 @@
SlideCraft DeckCraft
Copyright (c) 2026 ArtCraft Team and the SlideCraft contributors. Copyright (c) 2026 ArtCraft Team and the DeckCraft contributors.
Licensed under the MIT License or the Apache License, Version 2.0, at your option Licensed under the MIT License or the Apache License, Version 2.0, at your option
(LICENSE-MIT, LICENSE-APACHE). (LICENSE-MIT, LICENSE-APACHE).

View File

@ -7,7 +7,7 @@
</a> </a>
</p> </p>
<h1 align="center">SlideCraft</h1> <h1 align="center">DeckCraft</h1>
<p align="center"> <p align="center">
<b>Presentations and slide shows; an open-source, clean-room reimplementation of Microsoft PowerPoint, rebuilt in pure Rust.</b> <b>Presentations and slide shows; an open-source, clean-room reimplementation of Microsoft PowerPoint, rebuilt in pure Rust.</b>
@ -32,7 +32,7 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://getartcraft.com/apps/slidecraft"><b>SlideCraft on getartcraft.com</b></a> · <a href="https://getartcraft.com/apps/deckcraft"><b>DeckCraft on getartcraft.com</b></a> ·
<a href="https://getartcraft.com/">ArtCraft</a> · <a href="https://getartcraft.com/">ArtCraft</a> ·
<a href="https://getartcraft.com/apps">All Crafting Apps</a> <a href="https://getartcraft.com/apps">All Crafting Apps</a>
</p> </p>
@ -53,7 +53,7 @@
## What works today ## 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 - **Presentation model** with slide masters, eleven layouts, placeholders that inherit position and
formatting, themes (colour, font and effect schemes), sections, notes, comments, transitions 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 ## Getting started
```sh ```sh
git clone https://github.com/storytold/slidecraft git clone https://github.com/storytold/deckcraft
cd slidecraft cd deckcraft
cargo run --release -p slidecraft-cli -- render --sample --all --scale 1 out/ 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 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. fonts.
## Agents, CLI and MCP ## 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: same verbs:
```sh ```sh
slidecraft-cli commands format. # list commands deckcraft-cli commands format. # list commands
slidecraft-cli run --sample --cmd 'slide.new={"layout":"titleOnly","title":"Hello"}' --save hello.slidecraft deckcraft-cli run --sample --cmd 'slide.new={"layout":"titleOnly","title":"Hello"}' --save hello.deckcraft
slidecraft-cli mcp # MCP server over stdio (headless) deckcraft-cli mcp # MCP server over stdio (headless)
slidecraft-cli mcp --connect 7979 # drive the running app (slidecraft --control 7979) deckcraft-cli mcp --connect 7979 # drive the running app (deckcraft --control 7979)
``` ```
## Roadmap ## Roadmap
@ -97,7 +97,7 @@ See [ROADMAP.md](ROADMAP.md) for milestones, parity estimates and what's next.
## The Crafting Apps ## 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 [ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to
stand on its own. stand on its own.
@ -110,7 +110,7 @@ stand on its own.
| <img src="https://raw.githubusercontent.com/storytold/printcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.printcraft.png" alt="" width="32" height="32"> | **PrintCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/printcraft) | [Website](https://getartcraft.com/apps/printcraft) | | <img src="https://raw.githubusercontent.com/storytold/printcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.printcraft.png" alt="" width="32" height="32"> | **PrintCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/printcraft) | [Website](https://getartcraft.com/apps/printcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) | | <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) | | <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
| <img src="https://raw.githubusercontent.com/storytold/slidecraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.slidecraft.png" alt="" width="32" height="32"> | **SlideCraft** | **Presentations and slide shows · you are here** | [GitHub](https://github.com/storytold/slidecraft) | [Website](https://getartcraft.com/apps/slidecraft) | | <img src="https://raw.githubusercontent.com/storytold/deckcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png" alt="" width="32" height="32"> | **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. 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
<a href="https://discord.gg/artcraft"><b>discord.gg/artcraft</b></a> · <a href="https://discord.gg/artcraft"><b>discord.gg/artcraft</b></a> ·
<a href="https://getartcraft.com/">getartcraft.com</a> · <a href="https://getartcraft.com/">getartcraft.com</a> ·
<a href="https://getartcraft.com/apps">The Crafting Apps</a> · <a href="https://getartcraft.com/apps">The Crafting Apps</a> ·
<a href="https://getartcraft.com/apps/slidecraft">SlideCraft</a> <a href="https://getartcraft.com/apps/deckcraft">DeckCraft</a>
</p> </p>
## License and credits ## License and credits
SlideCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option. DeckCraft 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). 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 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). 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. 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 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 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. Forks and modified versions must remove them.
<sub>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.</sub> <sub>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.</sub>
<p align="center"> <p align="center">
<a href="https://getartcraft.com/"><img alt="ArtCraft" src="docs/brand/artcraft-mark.svg" width="28"></a><br> <a href="https://getartcraft.com/"><img alt="ArtCraft" src="docs/brand/artcraft-mark.svg" width="28"></a><br>

View File

@ -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 format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
(macOS, Windows, Linux, BSD and the web). (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…). - 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 - UI: PowerPoint-style ribbon with contextual tabs, ~240 original icons, status bar, panes, command
palette, light/dark. 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). 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. 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 - 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 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`. secrets live in the `release` environment. Version: `cargo xtask version`.
**In progress:** PPTX import/export · first signed release run. **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,
**Next (in order):** vector PDF artwork · WMA/WMV decoding · Format Shape pane depth · connectors with picture/texture options) · edit points · native macOS menu bar · print · Notes/Handout masters ·
glue, freeform and edit points · merge shapes · Animation Pane polish · native macOS menu bar · equations · SVG pictures · WMA/WMV decoding.
print · Notes/Handout masters · equations · SVG pictures.
Milestone details live in `plan/execution-plan.md` (M0–M14, local planning notes). 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) ## 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 **Breadth: ~79% weighted** (P0 core 93%, P1 74%, P2 35%) over the 187 features of the PowerPoint catalogue, scored row by
catalogue, scored row by row in [docs/parity.md](docs/parity.md) (`cargo xtask parity` recomputes row in [docs/parity.md](docs/parity.md) (`cargo xtask parity` recomputes it). Many features scored
it). Many features scored done still lack some of PowerPoint's options, dialogs or pixel fidelity, so done still lack some of PowerPoint's options, dialogs or pixel fidelity, so **overall parity
**overall parity including depth is about 55%**. including depth is about 62%**.
**Remaining work: about 230 wall-clock hours of a single Claude Opus 5.5 agent** (±30%), or roughly **Remaining work to 100%: about 190 wall-clock hours of a single Claude Opus 5.5 agent** (±30%),
80–110 hours with four agents in parallel on separate crates: or roughly 65–90 hours with four agents in parallel on separate crates:
| Work | Estimate | | Work | Estimate |
|---|---| |---|---|
| Open P0 (16 partial, 1 missing: PPTX fidelity, vector PDF, media playback, Format Shape pane, presenter view…) | 30 h | | Alpha blockers above | 35 h |
| Open P1 (23 partial, 10 missing: connectors/glue, freeform, merge shapes, print, multi-monitor, SVG…) | 45 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 (11 partial, 28 missing: equations, video export, 3-D, remove background, thesaurus…) | 40 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 | 75 h | | Depth and pixel fidelity of every ribbon group, dialog and pane against PowerPoint | 50 h |
| Performance (incremental rendering, GPU raster) and PPTX corpus hardening | 30 h | | Performance (incremental rendering, GPU raster, streaming media decode) | 20 h |
| Platform verification and release hardening (signing, Flatpak, BSD, web) | 10 h |
Basis: the M0 arc (model, renderer, text engine, engine with ~200 commands, ribbon UI, show engine, Basis: in this session the engine, renderer, UI, show engine, PPTX, PDF, media and release
CLI and MCP) took roughly 25 wall-clock agent hours; open catalogue rows average 1–2 hours each. 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 ## Agents: CLI and MCP
Every command is reachable from `slidecraft-cli` (`run`, `describe`, `commands`, `app` for the running Every command is reachable from `deckcraft-cli` (`run`, `describe`, `commands`, `app` for the running
window, `render`, `convert`), from MCP (`slidecraft-cli mcp`, optionally `--connect PORT`), and from the window, `render`, `convert`), from MCP (`deckcraft-cli mcp`, optionally `--connect PORT`), and from the
app's JSON control channel (`slidecraft --control PORT`). app's JSON control channel (`deckcraft --control PORT`).

View File

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

View File

@ -1,23 +1,23 @@
//! Headless SlideCraft. //! Headless DeckCraft.
//! //!
//! ```text //! ```text
//! slidecraft-cli info FILE # slides, titles, layouts (JSON) //! deckcraft-cli info FILE # slides, titles, layouts (JSON)
//! slidecraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] OUT.png|OUT_DIR //! deckcraft-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) //! deckcraft-cli convert IN OUT # .deckcraft ⇄ .pptx, outline .txt, .png (first slide)
//! slidecraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print] //! deckcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print]
//! slidecraft-cli commands [FILTER] # list commands (JSON) //! deckcraft-cli commands [FILTER] # list commands (JSON)
//! slidecraft-cli describe ID # one command //! deckcraft-cli describe ID # one command
//! slidecraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] # drive the running app //! deckcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] # drive the running app
//! slidecraft-cli mcp [--connect PORT] [--sample] # MCP server over stdio //! deckcraft-cli mcp [--connect PORT] [--sample] # MCP server over stdio
//! slidecraft-cli links | --version //! deckcraft-cli links | --version
//! ``` //! ```
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
use std::process::ExitCode; use std::process::ExitCode;
use deckcraft_engine::Session;
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_engine::Session;
macro_rules! outln { macro_rules! outln {
($($arg:tt)*) => {{ ($($arg:tt)*) => {{
@ -26,28 +26,28 @@ macro_rules! outln {
if e.kind() == std::io::ErrorKind::BrokenPipe { if e.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(0); 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); std::process::exit(1);
} }
}}; }};
} }
const USAGE: &str = "usage: slidecraft-cli info FILE const USAGE: &str = "usage: deckcraft-cli info FILE
slidecraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] OUT deckcraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] OUT
slidecraft-cli convert IN OUT deckcraft-cli convert IN OUT
slidecraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print] deckcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print]
slidecraft-cli commands [FILTER] deckcraft-cli commands [FILTER]
slidecraft-cli describe COMMAND deckcraft-cli describe COMMAND
slidecraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] deckcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]
slidecraft-cli mcp [--connect PORT] [--sample] deckcraft-cli mcp [--connect PORT] [--sample]
slidecraft-cli links | --version"; deckcraft-cli links | --version";
fn main() -> ExitCode { fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect(); let args: Vec<String> = std::env::args().skip(1).collect();
let rest = args.get(1..).unwrap_or(&[]); let rest = args.get(1..).unwrap_or(&[]);
match args.first().map(String::as_str) { match args.first().map(String::as_str) {
Some("--version" | "-V" | "version") => { Some("--version" | "-V" | "version") => {
outln!("slidecraft-cli {}", env!("CARGO_PKG_VERSION")); outln!("deckcraft-cli {}", env!("CARGO_PKG_VERSION"));
ExitCode::SUCCESS ExitCode::SUCCESS
} }
Some("info") => report(info(rest)), Some("info") => report(info(rest)),
@ -70,13 +70,13 @@ fn main() -> ExitCode {
Some("app") => report(app(rest)), Some("app") => report(app(rest)),
Some("mcp") => report(mcp(rest)), Some("mcp") => report(mcp(rest)),
Some("links") => { Some("links") => {
use slidecraft_engine::links::*; use deckcraft_engine::links::*;
outln!("Discord {DISCORD}\nWebsite {WEBSITE}\nApp page {APP_PAGE}\nGitHub {GITHUB}"); outln!("Discord {DISCORD}\nWebsite {WEBSITE}\nApp page {APP_PAGE}\nGitHub {GITHUB}");
ExitCode::SUCCESS ExitCode::SUCCESS
} }
_ => { _ => {
eprintln!("{USAGE}"); 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 ExitCode::FAILURE
} }
} }
@ -86,7 +86,7 @@ fn report(r: Result<(), String>) -> ExitCode {
match r { match r {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(e) => { Err(e) => {
eprintln!("slidecraft-cli: {e}"); eprintln!("deckcraft-cli: {e}");
ExitCode::FAILURE ExitCode::FAILURE
} }
} }
@ -94,7 +94,7 @@ fn report(r: Result<(), String>) -> ExitCode {
fn open(s: &mut Session, input: Option<&str>, sample: bool) -> Result<(), String> { fn open(s: &mut Session, input: Option<&str>, sample: bool) -> Result<(), String> {
if sample { 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 { match input {
Some(p) => s.execute("file.open", &json!({"path": p})).map(|_| ()).map_err(|e| e.to_string()), 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 { if i >= n {
return Err(format!("no slide {i} (the deck has {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() }; 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}"))?; std::fs::write(&path, png).map_err(|e| format!("{path}: {e}"))?;
outln!("{path} ({w}×{h})"); 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 (Some(input), Some(out)) = (args.first(), args.get(1)) else { return Err("convert needs IN and OUT".into()) };
let mut s = Session::new(); let mut s = Session::new();
open(&mut s, Some(input), false)?; 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") { let r = if matches!(fmt, "png" | "jpeg") {
s.execute("file.export", &json!({"path": out, "slide": 0})) s.execute("file.export", &json!({"path": out, "slide": 0}))
} else { } else {
@ -218,14 +218,14 @@ fn run(args: &[String]) -> Result<(), String> {
fn describe(id: Option<&str>) -> Result<(), String> { fn describe(id: Option<&str>) -> Result<(), String> {
let id = id.ok_or("describe needs a command id")?; 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); outln!("{} — {}\n where: {}\n shortcut: {}\n params: {}", c.id, c.label, c.menu.join(" ▸ "), c.shortcut.unwrap_or("—"), c.params);
Ok(()) Ok(())
} }
fn app(args: &[String]) -> Result<(), String> { fn app(args: &[String]) -> Result<(), String> {
use slidecraft_mcp::{Backend, Remote, control_addr}; use deckcraft_mcp::{Backend, Remote, control_addr};
let mut port = slidecraft_mcp::DEFAULT_PORT.to_string(); let mut port = deckcraft_mcp::DEFAULT_PORT.to_string();
let mut method: Option<String> = None; let mut method: Option<String> = None;
let mut rest = vec![]; let mut rest = vec![];
let mut it = args.iter(); let mut it = args.iter();
@ -237,7 +237,7 @@ fn app(args: &[String]) -> Result<(), String> {
} }
} }
let mut r = Remote::connect(&control_addr(&port)) 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<Value, String> { let parse = |s: Option<&String>| -> Result<Value, String> {
s.map(|j| serde_json::from_str(j).map_err(|e| format!("bad JSON: {e}"))).transpose().map(|v| v.unwrap_or(json!({}))) 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> { 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<String> = None; let mut connect: Option<String> = None;
let mut sample = false; let mut sample = false;
let mut it = args.iter(); let mut it = args.iter();
@ -269,7 +269,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
None => { None => {
let mut h = Headless::new(); let mut h = Headless::new();
if sample { 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) Box::new(h)
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "slidecraft-web" name = "deckcraft-web"
description = "SlideCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)" description = "DeckCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -10,12 +10,12 @@ publish = false
workspace = true workspace = true
[[bin]] [[bin]]
name = "slidecraft-web" name = "deckcraft-web"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
slidecraft-engine = { workspace = true } deckcraft-engine = { workspace = true }
slidecraft-ui-egui = { workspace = true } deckcraft-ui-egui = { workspace = true }
egui = { workspace = true } egui = { workspace = true }
log = { workspace = true } log = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }

View File

@ -1,4 +1,4 @@
# Build with: trunk build --release (from apps/slidecraft-web) # Build with: trunk build --release (from apps/deckcraft-web)
[build] [build]
target = "index.html" target = "index.html"
dist = "../../dist/web" dist = "../../dist/web"

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>DeckCraft</title>
<link data-trunk rel="rust" data-bin="deckcraft-web" data-wasm-opt="z" data-weak-refs />
<link data-trunk rel="icon" href="../../assets/app-icon/hicolor/32x32/apps/ai.storyteller.deckcraft.png" />
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #262626; }
#deckcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#deckcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #b8b8b8; font: 14px system-ui, sans-serif; pointer-events: none; }
</style>
</head>
<body>
<canvas id="deckcraft_canvas"></canvas>
<div id="deckcraft_loading">Loading DeckCraft&hellip;</div>
</body>
</html>

View File

@ -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` //! runner (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release`
//! from this directory (output in `dist/web`). //! from this directory (output in `dist/web`).
//! //!
@ -27,5 +27,5 @@ fn main() {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn main() { 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");
} }

View File

@ -1,15 +1,15 @@
//! The browser shell: web `Services`, drag-and-drop, and the eframe web runner. //! The browser shell: web `Services`, drag-and-drop, and the eframe web runner.
use slidecraft_engine::Session; use deckcraft_engine::Session;
use slidecraft_ui_egui::{Inbox, Services, SlideApp}; use deckcraft_ui_egui::{Inbox, Services, SlideApp};
use wasm_bindgen::JsCast as _; 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 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 AUDIO_EXTS: &[&str] = &["wav", "mp3", "m4a", "aac", "flac", "ogg", "opus", "aif", "aiff"];
const VIDEO_EXTS: &[&str] = &["mp4", "m4v", "mov", "webm"]; const VIDEO_EXTS: &[&str] = &["mp4", "m4v", "mov", "webm"];
const CANVAS_ID: &str = "slidecraft_canvas"; const CANVAS_ID: &str = "deckcraft_canvas";
const LOADING_ID: &str = "slidecraft_loading"; const LOADING_ID: &str = "deckcraft_loading";
pub fn start() { pub fn start() {
eframe::WebLogger::init(log::LevelFilter::Info).ok(); eframe::WebLogger::init(log::LevelFilter::Info).ok();
@ -34,13 +34,13 @@ pub fn start() {
options, options,
Box::new(move |cc| { Box::new(move |cc| {
if let Some(rs) = &cc.wgpu_render_state { 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 inbox: Inbox = Inbox::default();
let mut app = SlideApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone())); let mut app = SlideApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone()));
let q = query(); let q = query();
if !q.contains("blank") { 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}"); log::error!("sample deck: {e}");
} }
} else if let Err(e) = app.run("file.new", serde_json::json!({})) { } 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) { if let Some(el) = document.get_element_by_id(LOADING_ID) {
match result { match result {
Ok(()) => el.remove(), Ok(()) => el.remove(),
Err(e) => el.set_inner_html(&format!("<p>SlideCraft failed to start: {e:?}</p><p>A browser with WebGPU or WebGL2 is required.</p>")), Err(e) => el.set_inner_html(&format!("<p>DeckCraft failed to start: {e:?}</p><p>A browser with WebGPU or WebGL2 is required.</p>")),
} }
} }
}); });
@ -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`. /// Trigger a browser download of `bytes` named after the last component of `path`.
fn download(path: &str, bytes: &[u8]) -> Result<(), String> { fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
let js = |e: wasm_bindgen::JsValue| format!("{e:?}"); 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 window = web_sys::window().ok_or("no window")?;
let document = window.document().ok_or("no document")?; let document = window.document().ok_or("no document")?;
let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes)); 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("pdf") => "application/pdf",
Some("txt") => "text/plain", Some("txt") => "text/plain",
Some("pptx") => "application/vnd.openxmlformats-officedocument.presentationml.presentation", Some("pptx") => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
Some("slidecraft") => "application/zip", Some("deckcraft") => "application/zip",
_ => "application/octet-stream", _ => "application/octet-stream",
} }
} }

View File

@ -1,26 +1,26 @@
[package] [package]
name = "slidecraft" name = "deckcraft"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
description = "SlideCraft desktop app" description = "DeckCraft desktop app"
publish = false publish = false
default-run = "slidecraft" default-run = "deckcraft"
[lints] [lints]
workspace = true workspace = true
[dependencies] [dependencies]
slidecraft-engine = { workspace = true } deckcraft-engine = { workspace = true }
slidecraft-ui-egui = { workspace = true } deckcraft-ui-egui = { workspace = true }
slidecraft-media = { workspace = true } deckcraft-media = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] } eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] }
egui = { workspace = true } egui = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
log = { workspace = true } log = { workspace = true }
[build-dependencies] [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" winresource = "0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]

30
apps/deckcraft/build.rs Normal file
View File

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

View File

@ -1,8 +1,8 @@
//! cpal audio output for media playback: the device pulls mixed samples from the //! 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 cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use slidecraft_media::AudioOut; use deckcraft_media::AudioOut;
#[derive(Default)] #[derive(Default)]
pub struct CpalOut { pub struct CpalOut {

View File

@ -1,24 +1,24 @@
//! Loopback JSON-lines control server: one request per line, one reply per line. //! 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::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream}; use std::net::{TcpListener, TcpStream};
use std::sync::mpsc::{Receiver, Sender, channel}; use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration; use std::time::Duration;
use deckcraft_ui_egui::ControlRequest;
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_ui_egui::ControlRequest;
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> { pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
let (tx, rx) = channel::<ControlRequest>(); let (tx, rx) = channel::<ControlRequest>();
let listener = match TcpListener::bind(("127.0.0.1", port)) { let listener = match TcpListener::bind(("127.0.0.1", port)) {
Ok(l) => l, Ok(l) => l,
Err(e) => { 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; 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 || { std::thread::spawn(move || {
for stream in listener.incoming().flatten() { for stream in listener.incoming().flatten() {
let tx = tx.clone(); let tx = tx.clone();

View File

@ -1,10 +1,10 @@
//! SlideCraft desktop app. //! DeckCraft desktop app.
//! //!
//! Usage: `slidecraft [--control <port>] [--sample] [--show] [files…]` //! Usage: `deckcraft [--control <port>] [--sample] [--show] [files…]`
//! //!
//! `--control <port>` (or `SLIDECRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server: //! `--control <port>` (or `DECKCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server:
//! `{"id":1,"method":"ui.inspect","params":{}}` → `{"id":1,"ok":true,"result":…}`. //! `{"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")] #![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)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@ -12,8 +12,8 @@
mod audio; mod audio;
mod control_server; mod control_server;
use slidecraft_engine::Session; use deckcraft_engine::Session;
use slidecraft_ui_egui::{Services, SlideApp}; use deckcraft_ui_egui::{Services, SlideApp};
struct App(SlideApp); struct App(SlideApp);
@ -34,38 +34,38 @@ impl eframe::App for App {
fn prefs_path() -> Option<std::path::PathBuf> { fn prefs_path() -> Option<std::path::PathBuf> {
let base = if cfg!(target_os = "macos") { 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) { } 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 { } else {
std::env::var_os("XDG_CONFIG_HOME") std::env::var_os("XDG_CONFIG_HOME")
.map(std::path::PathBuf::from) .map(std::path::PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))) .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")) base.map(|b| b.join("ui.json"))
} }
fn load_prefs(app: &mut SlideApp) { 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; return;
} }
if let Some(p) = prefs_path() if let Some(p) = prefs_path()
&& let Ok(bytes) = std::fs::read(&p) && let Ok(bytes) = std::fs::read(&p)
&& let Ok(ui) = serde_json::from_slice::<slidecraft_ui_egui::UiState>(&bytes) && let Ok(ui) = serde_json::from_slice::<deckcraft_ui_egui::UiState>(&bytes)
{ {
app.ui = ui; app.ui = ui;
} }
if let Some(p) = prefs_path().map(|p| p.with_file_name("prefs.json")) if let Some(p) = prefs_path().map(|p| p.with_file_name("prefs.json"))
&& let Ok(bytes) = std::fs::read(&p) && let Ok(bytes) = std::fs::read(&p)
&& let Ok(prefs) = serde_json::from_slice::<slidecraft_engine::Prefs>(&bytes) && let Ok(prefs) = serde_json::from_slice::<deckcraft_engine::Prefs>(&bytes)
{ {
app.session.prefs = prefs; app.session.prefs = prefs;
} }
} }
fn save_prefs(app: &SlideApp) { 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; return;
} }
if let Some(p) = prefs_path() { if let Some(p) = prefs_path() {
@ -91,8 +91,8 @@ fn services() -> Services {
), ),
"video" => d.add_filter("Video", &["mp4", "m4v", "mov", "webm", "mkv", "wmv"]), "video" => d.add_filter("Video", &["mp4", "m4v", "mov", "webm", "mkv", "wmv"]),
_ => d _ => d
.add_filter("Presentations", &["slidecraft", "pptx", "potx", "ppsx"]) .add_filter("Presentations", &["deckcraft", "pptx", "potx", "ppsx"])
.add_filter("SlideCraft", &["slidecraft"]) .add_filter("DeckCraft", &["deckcraft"])
.add_filter("PowerPoint", &["pptx", "potx", "ppsx"]) .add_filter("PowerPoint", &["pptx", "potx", "ppsx"])
.add_filter("Outline", &["txt", "md"]), .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()?; rgba.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?;
Some(out) Some(out)
})), })),
// Media is mixed and clocked by the output device; `SLIDECRAFT_NO_AUDIO` plays silently. // Media is mixed and clocked by the output device; `DECKCRAFT_NO_AUDIO` plays silently.
audio_out: std::env::var_os("SLIDECRAFT_NO_AUDIO") audio_out: std::env::var_os("DECKCRAFT_NO_AUDIO")
.is_none() .is_none()
.then(|| Box::new(audio::CpalOut::default()) as Box<dyn slidecraft_media::AudioOut>), .then(|| Box::new(audio::CpalOut::default()) as Box<dyn deckcraft_media::AudioOut>),
..Default::default() ..Default::default()
} }
} }
const APP_ID: &str = "ai.storyteller.slidecraft"; const APP_ID: &str = "ai.storyteller.deckcraft";
fn app_icon() -> Option<egui::IconData> { fn app_icon() -> Option<egui::IconData> {
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() eframe::icon_data::from_png_bytes(png).map_err(|e| log::warn!("app icon: {e}")).ok()
} }
fn main() -> eframe::Result { fn main() -> eframe::Result {
let mut control_port: Option<u16> = std::env::var("SLIDECRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); let mut control_port: Option<u16> = std::env::var("DECKCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
let mut files = Vec::new(); let mut files = Vec::new();
let mut sample = false; let mut sample = false;
let mut show = false; let mut show = false;
@ -136,7 +136,7 @@ fn main() -> eframe::Result {
"--sample" => sample = true, "--sample" => sample = true,
"--show" => show = true, "--show" => show = true,
"--version" => { "--version" => {
println!("slidecraft {}", env!("CARGO_PKG_VERSION")); println!("deckcraft {}", env!("CARGO_PKG_VERSION"));
return Ok(()); return Ok(());
} }
_ => files.push(a), _ => files.push(a),
@ -144,7 +144,7 @@ fn main() -> eframe::Result {
} }
let mut options = eframe::NativeOptions { let mut options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_title("SlideCraft") .with_title("DeckCraft")
.with_inner_size([1440.0, 900.0]) .with_inner_size([1440.0, 900.0])
.with_min_inner_size([900.0, 560.0]) .with_min_inner_size([900.0, 560.0])
.with_drag_and_drop(true) .with_drag_and_drop(true)
@ -158,21 +158,21 @@ fn main() -> eframe::Result {
options.viewport = options.viewport.with_icon(icon); options.viewport = options.viewport.with_icon(icon);
} }
#[cfg(all(unix, not(target_os = "macos")))] #[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| { options.event_loop_builder = Some(Box::new(|b| {
use winit::platform::x11::EventLoopBuilderExtX11; use winit::platform::x11::EventLoopBuilderExtX11;
b.with_x11(); b.with_x11();
})); }));
} }
eframe::run_native( eframe::run_native(
"SlideCraft", "DeckCraft",
options, options,
Box::new(move |cc| { Box::new(move |cc| {
let mut app = SlideApp::new(Session::new(), services()); let mut app = SlideApp::new(Session::new(), services());
load_prefs(&mut app); load_prefs(&mut app);
// AutoRecover: reopen what a previous run left unsaved, then keep it current. // AutoRecover: reopen what a previous run left unsaved, then keep it current.
if std::env::var_os("SLIDECRAFT_NO_RECOVERY").is_none() { if std::env::var_os("DECKCRAFT_NO_RECOVERY").is_none() {
app.session.recovery_dir = slidecraft_engine::recovery::default_dir(); app.session.recovery_dir = deckcraft_engine::recovery::default_dir();
if let Ok(v) = app.session.execute("file.recovery.open", &serde_json::json!({})) 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) && 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()); let rx = control_server::start(port, cc.egui_ctx.clone());
app = app.with_control(rx); app = app.with_control(rx);
} }
if sample && let Err(e) = slidecraft_engine::sample::open_sample(&mut app.session) { if sample && let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) {
eprintln!("slidecraft: sample: {e}"); eprintln!("deckcraft: sample: {e}");
} }
for f in files { for f in files {
if let Err(e) = app.open_path(&f) { if let Err(e) = app.open_path(&f) {
eprintln!("slidecraft: {f}: {e}"); eprintln!("deckcraft: {f}: {e}");
} }
} }
if show { if show {

View File

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

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>SlideCraft</title>
<link data-trunk rel="rust" data-bin="slidecraft-web" data-wasm-opt="z" data-weak-refs />
<link data-trunk rel="icon" href="../../assets/app-icon/hicolor/32x32/apps/ai.storyteller.slidecraft.png" />
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #262626; }
#slidecraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#slidecraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #b8b8b8; font: 14px system-ui, sans-serif; pointer-events: none; }
</style>
</head>
<body>
<canvas id="slidecraft_canvas"></canvas>
<div id="slidecraft_loading">Loading SlideCraft&hellip;</div>
</body>
</html>

View File

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

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 818 B

After

Width:  |  Height:  |  Size: 818 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,14 +1,14 @@
[package] [package]
name = "slidecraft-anim" name = "deckcraft-anim"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.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] [lints]
workspace = true workspace = true
[dependencies] [dependencies]
slidecraft-model = { workspace = true } deckcraft-model = { workspace = true }
slidecraft-geom = { workspace = true } deckcraft-geom = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View File

@ -2,7 +2,7 @@
use std::f64::consts::PI; use std::f64::consts::PI;
use slidecraft_model::{AnimClass, Rgba, Xfrm}; use deckcraft_model::{AnimClass, Rgba, Xfrm};
use crate::AnimState; use crate::AnimState;
use crate::easing::{bounce_out, ease, smooth}; use crate::easing::{bounce_out, ease, smooth};

View File

@ -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 //! - [`Timeline`]: a slide's animation list grouped into click steps, with per-shape (and
//! per-paragraph) [`AnimState`] at any point of the show. //! per-paragraph) [`AnimState`] at any point of the show.
@ -22,8 +22,8 @@ mod transition;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub use deckcraft_model::Rgba;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use slidecraft_model::Rgba;
pub use easing::ease; pub use easing::ease;
pub use morph::{morph_pairs, morph_xfrm}; pub use morph::{morph_pairs, morph_xfrm};

View File

@ -1,6 +1,6 @@
//! Morph: matching shapes between two slides and interpolating their boxes. //! 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; use crate::clampf;

View File

@ -1,7 +1,7 @@
//! Slide show navigation: click steps, slide order (hidden slides, custom shows, ranges), loop //! Slide show navigation: click steps, slide order (hidden slides, custom shows, ranges), loop
//! and automatic advance. //! and automatic advance.
use slidecraft_model::Presentation; use deckcraft_model::Presentation;
use crate::Timeline; use crate::Timeline;

View File

@ -2,8 +2,8 @@
use std::sync::Arc; use std::sync::Arc;
use slidecraft_model::anim::{ANIMATIONS, TRANSITIONS, TextBuild}; use deckcraft_model::anim::{ANIMATIONS, TRANSITIONS, TextBuild};
use slidecraft_model::{ use deckcraft_model::{
AnimClass, AnimStart, Animation, ColorRef, CustomShow, Presentation, Rgba, Shape, ShapeId, Slide, SlideId, TextBody, Transition, Xfrm, AnimClass, AnimStart, Animation, ColorRef, CustomShow, Presentation, Rgba, Shape, ShapeId, Slide, SlideId, TextBody, Transition, Xfrm,
}; };

View File

@ -1,8 +1,8 @@
//! A slide's animation sequence: click steps, effect start times and per-shape state over time. //! A slide's animation sequence: click steps, effect start times and per-shape state over time.
use slidecraft_model::anim::TextBuild; use deckcraft_model::anim::TextBuild;
use slidecraft_model::style::Fill; use deckcraft_model::style::Fill;
use slidecraft_model::{AnimClass, AnimStart, Animation, ColorScheme, Rgba, SchemeSlot, ShapeId, Slide, Xfrm}; use deckcraft_model::{AnimClass, AnimStart, Animation, ColorScheme, Rgba, SchemeSlot, ShapeId, Slide, Xfrm};
use crate::AnimState; use crate::AnimState;
use crate::effects::{EffectSpec, exit_base, motion_path}; 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<ColorScheme> { fn default_scheme() -> Option<ColorScheme> {
slidecraft_model::theme::builtin_color_schemes().into_iter().next() deckcraft_model::theme::builtin_color_schemes().into_iter().next()
} }
impl Timeline { impl Timeline {
@ -139,11 +139,11 @@ impl Timeline {
owned.as_ref() owned.as_ref()
} }
}; };
let resolve = |c: &slidecraft_model::ColorRef| -> Rgba { let resolve = |c: &deckcraft_model::ColorRef| -> Rgba {
match scheme { match scheme {
Some(s) => c.resolve(s, None), Some(s) => c.resolve(s, None),
None => match &c.base { None => match &c.base {
slidecraft_model::ColorBase::Rgb { rgb } => *rgb, deckcraft_model::ColorBase::Rgb { rgb } => *rgb,
_ => Rgba::rgb(0x15, 0x60, 0x82), _ => Rgba::rgb(0x15, 0x60, 0x82),
}, },
} }
@ -180,7 +180,7 @@ impl Timeline {
(None, TextBuild::ByParagraph) => { (None, TextBuild::ByParagraph) => {
let mut v: Vec<Option<usize>> = vec![]; let mut v: Vec<Option<usize>> = vec![];
if let Some(t) = shape.and_then(|s| s.text.as_ref()) { 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 top = t.paragraphs.iter().filter(|p| filled(p)).map(|p| p.level).min().unwrap_or(0);
let mut owner: Option<usize> = None; let mut owner: Option<usize> = None;
for (i, p) in t.paragraphs.iter().enumerate() { 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)) 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 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 }; let path = if a.class == AnimClass::Path { Some(motion_path(&a.effect, &a.option, a.path.as_deref())) } else { None };
EffectSpec { EffectSpec {

View File

@ -196,7 +196,7 @@ fn scale_rect(r: R, s: f64) -> R {
// ---------- effects ---------- // ---------- 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; /// 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 /// at `t <= 0` (or NaN) the old slide alone. Morph returns a plain crossfade (the UI renders morph
/// frames with [`crate::morph_pairs`]). /// frames with [`crate::morph_pairs`]).
@ -736,7 +736,7 @@ fn layers(kind: &str, option: &str, t: f64, depth: u32) -> Vec<Layer> {
"random" if depth == 0 => { "random" if depth == 0 => {
let h = option.bytes().fold(0x51ED_u64, |h, b| hash64(h ^ b as u64)); 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 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) layers(k, opt, t, depth + 1)
} }
_ => crossfade(t), _ => crossfade(t),

View File

@ -1,5 +1,5 @@
[package] [package]
name = "slidecraft-av1" name = "deckcraft-av1"
description = "Clean-room AV1 decoder (AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1)" description = "Clean-room AV1 decoder (AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1)"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
@ -14,6 +14,6 @@ default = ["threads"]
threads = ["dep:rayon"] threads = ["dep:rayon"]
[dependencies] [dependencies]
slidecraft-bitstream = { workspace = true } deckcraft-bitstream = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
rayon = { workspace = true, optional = true } rayon = { workspace = true, optional = true }

View File

@ -1,5 +1,5 @@
[package] [package]
name = "slidecraft-bitstream" name = "deckcraft-bitstream"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true

View File

@ -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. //! - [`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). //! - [`BitWriter`]: the matching writer (for encoders and header rewriting).

View File

@ -1,9 +1,9 @@
[package] [package]
name = "slidecraft-color" name = "deckcraft-color"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.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] [lints]
workspace = true workspace = true

View File

@ -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 //! (tint, shade, luminance modulation/offset, alpha…), so theme-relative colours resolve the way
//! presentation files expect. //! presentation files expect.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]

View File

@ -1,24 +1,24 @@
[package] [package]
name = "slidecraft-engine" name = "deckcraft-engine"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.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] [lints]
workspace = true workspace = true
[dependencies] [dependencies]
slidecraft-geom = { workspace = true } deckcraft-geom = { workspace = true }
slidecraft-color = { workspace = true } deckcraft-color = { workspace = true }
slidecraft-model = { workspace = true } deckcraft-model = { workspace = true }
slidecraft-fonts = { workspace = true } deckcraft-fonts = { workspace = true }
slidecraft-text = { workspace = true } deckcraft-text = { workspace = true }
slidecraft-render = { workspace = true } deckcraft-render = { workspace = true }
slidecraft-pdf = { workspace = true } deckcraft-pdf = { workspace = true }
slidecraft-format = { workspace = true } deckcraft-format = { workspace = true }
slidecraft-pptx = { workspace = true } deckcraft-pptx = { workspace = true }
slidecraft-media = { workspace = true } deckcraft-media = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
i_overlay = { workspace = true } i_overlay = { workspace = true }

View File

@ -2,8 +2,8 @@
use std::sync::Arc; use std::sync::Arc;
use deckcraft_model::anim::{ANIMATIONS, AnimClass, AnimStart, Animation, TextBuild, animation_info};
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_model::anim::{ANIMATIONS, AnimClass, AnimStart, Animation, TextBuild, animation_info};
use super::*; use super::*;
use crate::{Result, Session}; use crate::{Result, Session};
@ -227,7 +227,7 @@ fn timing(s: &mut Session, p: &Value) -> Result<Value> {
let delay = p.get("delay").and_then(Value::as_u64).map(|v| v.min(600_000) as u32); 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 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 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| { each_target(s, p, |a| {
if let Some(x) = start { if let Some(x) = start {
a.start = x; a.start = x;

View File

@ -1,8 +1,8 @@
//! Arrange: order, group, align, distribute, rotate and flip. //! 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 serde_json::{Value, json};
use slidecraft_geom::{Point, Rect, Xfrm};
use slidecraft_model::{Shape, ShapeId, ShapeKind};
use super::*; use super::*;
use crate::{Result, Session}; use crate::{Result, Session};
@ -145,7 +145,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
pub(crate) fn child_to_parent(group: &Xfrm, ch: &Xfrm, c: &Xfrm) -> Xfrm { 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 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 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 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 (w, h) = (c.w * sx.abs(), c.h * sy.abs());
let mut rot = c.rot + group.rot; let mut rot = c.rot + group.rot;

View File

@ -3,11 +3,11 @@
use std::sync::Arc; 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 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 super::*;
use crate::{Result, Session, Target}; use crate::{Result, Session, Target};
@ -114,7 +114,7 @@ fn colors(s: &mut Session, p: &Value) -> Result<Value> {
} }
if let Some(c) = &custom { if let Some(c) = &custom {
for (k, v) in c { 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); m.theme.colors.set(slot, rgb);
} }
} }
@ -134,10 +134,10 @@ fn fonts(s: &mut Session, p: &Value) -> Result<Value> {
} else { } else {
let major = str_param(p, "major").ok_or_else(|| bad("design.fonts", "give `name`, or `major` and `minor`"))?; 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); let minor = str_param(p, "minor").unwrap_or(major);
slidecraft_model::theme::FontScheme { deckcraft_model::theme::FontScheme {
name: "Custom".into(), name: "Custom".into(),
major: slidecraft_model::theme::FontSet { latin: major.into(), ..Default::default() }, major: deckcraft_model::theme::FontSet { latin: major.into(), ..Default::default() },
minor: slidecraft_model::theme::FontSet { latin: minor.into(), ..Default::default() }, minor: deckcraft_model::theme::FontSet { latin: minor.into(), ..Default::default() },
} }
}; };
s.edit(|doc, sel| { s.edit(|doc, sel| {
@ -155,16 +155,12 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
Some(Background::Fill { fill: Fill::solid(c) }) Some(Background::Fill { fill: Fill::solid(c) })
} else if let Some(i) = usize_param(p, "style") { } else if let Some(i) = usize_param(p, "style") {
// Background styles: 3 theme bg fills × 4 colours (bg1, bg2, tx1, tx2). // Background styles: 3 theme bg fills × 4 colours (bg1, bg2, tx1, tx2).
let slots = [ let slots =
slidecraft_color::SchemeSlot::Bg1, [deckcraft_color::SchemeSlot::Bg1, deckcraft_color::SchemeSlot::Bg2, deckcraft_color::SchemeSlot::Tx2, deckcraft_color::SchemeSlot::Tx1];
slidecraft_color::SchemeSlot::Bg2,
slidecraft_color::SchemeSlot::Tx2,
slidecraft_color::SchemeSlot::Tx1,
];
let i = i.clamp(1, 12) - 1; let i = i.clamp(1, 12) - 1;
Some(Background::Ref { Some(Background::Ref {
idx: 1001 + (i / 4) as u32, 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") { } 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`"))?; 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<Value> {
Some(Background::Fill { fill: Fill::Gradient(g) }) Some(Background::Fill { fill: Fill::Gradient(g) })
} else if let Some(pt) = p.get("pattern") { } else if let Some(pt) = p.get("pattern") {
Some(Background::Fill { 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(), 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)), 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(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Bg1)), 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() { } 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 (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 ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, 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 { } else {
return Err(bad("design.background", "give `color`, `gradient`, `pattern`, `picture`, `style` or `reset`")); 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; x.h *= sy;
} }
if let Some(t) = sh.text.as_mut() { 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() { if let Some(sz) = r.size.as_mut() {
*sz = (*sz * k).max(1.0); *sz = (*sz * k).max(1.0);
} }
@ -316,7 +312,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
for i in list { for i in list {
let Some(layout) = doc.slides.get(i).map(|x| x.layout) else { continue }; let Some(layout) = doc.slides.get(i).map(|x| x.layout) else { continue };
let lay = doc.layout(layout).map(|(_, l)| l.clone()); 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| { let want = |k: PhType| {
!(hf.hide_on_title && is_title) !(hf.hide_on_title && is_title)
&& match k { && match k {
@ -343,7 +339,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
let Some(lp) = lp else { continue }; let Some(lp) = lp else { continue };
let body = match k { let body = match k {
PhType::SlideNum => TextBody { PhType::SlideNum => TextBody {
paragraphs: vec![slidecraft_model::Paragraph { paragraphs: vec![deckcraft_model::Paragraph {
runs: vec![Run { runs: vec![Run {
text: "‹#›".into(), text: "‹#›".into(),
props: Default::default(), props: Default::default(),
@ -354,7 +350,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
..Default::default() ..Default::default()
}, },
PhType::Date if hf.date_text.is_empty() => TextBody { 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() } }], runs: vec![Run { text: today(), props: Default::default(), kind: RunKind::Field { field: "datetime1".into() } }],
..Default::default() ..Default::default()
}], }],
@ -444,7 +440,7 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").unwrap_or("Custom Layout").to_string(); let name = str_param(p, "name").unwrap_or("Custom Layout").to_string();
s.edit(|doc, sel| { s.edit(|doc, sel| {
let mi = master_index(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 tid = ShapeId(doc.alloc_id());
let size = doc.slide_size; let size = doc.slide_size;
let m = Arc::make_mut(doc.masters.get_mut(mi).ok_or_else(|| bad("master.insertLayout", "no master"))?); 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<Value> {
}; };
m.layouts.insert( m.layouts.insert(
at.min(m.layouts.len()), at.min(m.layouts.len()),
slidecraft_model::Layout { deckcraft_model::Layout {
id, id,
name, name,
kind: slidecraft_model::LayoutType::Custom, kind: deckcraft_model::LayoutType::Custom,
shapes: title.into_iter().collect(), shapes: title.into_iter().collect(),
background: None, background: None,
show_master_shapes: true, show_master_shapes: true,
@ -528,11 +524,11 @@ fn insert_placeholder(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad("master.insertPlaceholder", "select a layout in Slide Master view")); return Err(bad("master.insertPlaceholder", "select a layout in Slide Master view"));
} }
let size = s.doc()?.doc.slide_size; 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 idx = s.doc()?.shapes().iter().filter_map(|x| x.ph.as_ref().map(|p| p.idx)).max().unwrap_or(0) + 1;
let shape = Shape { let shape = Shape {
xfrm: Some(rect), 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()), text: Some(TextBody::default()),
..Default::default() ..Default::default()
}; };

View File

@ -2,8 +2,8 @@
use std::sync::Arc; use std::sync::Arc;
use deckcraft_model::{MediaId, Shape, ShapeId, Slide, SlideId};
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_model::{MediaId, Shape, ShapeId, Slide, SlideId};
use super::*; use super::*;
use crate::{HistoryEntry, Result, Session}; 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<MediaId>) { fn media_of_shapes(shapes: &[Shape], out: &mut Vec<MediaId>) {
slidecraft_model::walk(shapes, &mut |s, _| { deckcraft_model::walk(shapes, &mut |s, _| {
match &s.kind { match &s.kind {
slidecraft_model::ShapeKind::Picture { fill } => out.push(fill.media), deckcraft_model::ShapeKind::Picture { fill } => out.push(fill.media),
slidecraft_model::ShapeKind::Media(m) => { deckcraft_model::ShapeKind::Media(m) => {
out.push(m.media); out.push(m.media);
if let Some(p) = m.poster { if let Some(p) = m.poster {
out.push(p); out.push(p);
@ -60,7 +60,7 @@ fn media_of_shapes(shapes: &[Shape], out: &mut Vec<MediaId>) {
} }
_ => {} _ => {}
} }
if let Some(slidecraft_model::Fill::Picture(pf)) = &s.fill { if let Some(deckcraft_model::Fill::Picture(pf)) = &s.fill {
out.push(pf.media); out.push(pf.media);
} }
}); });
@ -73,8 +73,8 @@ fn copy_inner(s: &mut Session, p: &Value) -> Result<String> {
if t.is_range() { if t.is_range() {
let body = super::text::body_of(st, t).cloned().unwrap_or_default(); let body = super::text::body_of(st, t).cloned().unwrap_or_default();
let (a, b) = t.ordered(); let (a, b) = t.ordered();
let plain = slidecraft_model::edit::text_range(&body, a, b); let plain = deckcraft_model::edit::text_range(&body, a, b);
let sliced = slidecraft_model::edit::slice(&body, a, b); let sliced = deckcraft_model::edit::slice(&body, a, b);
s.clipboard = crate::Clipboard { text: Some(sliced), plain: plain.clone(), ..Default::default() }; s.clipboard = crate::Clipboard { text: Some(sliced), plain: plain.clone(), ..Default::default() };
return Ok(plain); return Ok(plain);
} }
@ -126,8 +126,8 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
} }
/// Give pasted shapes fresh ids (groups included) and map connector ends. /// Give pasted shapes fresh ids (groups included) and map connector ends.
pub(crate) fn reid(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape]) { pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) {
fn rec(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape], depth: usize) { fn rec(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape], depth: usize) {
if depth > 64 { if depth > 64 {
return; return;
} }
@ -141,7 +141,7 @@ pub(crate) fn reid(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape
rec(doc, shapes, 0); 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 { for m in media {
if doc.media(m.id).is_none_or(|x| x.data != m.data) { if doc.media(m.id).is_none_or(|x| x.data != m.data) {
if doc.media(m.id).is_none() { if doc.media(m.id).is_none() {
@ -319,7 +319,7 @@ pub(crate) fn remove_shapes(shapes: &mut Vec<Shape>, ids: &[ShapeId]) -> usize {
fn count(v: &[Shape]) -> usize { fn count(v: &[Shape]) -> usize {
let mut n = 0; let mut n = 0;
slidecraft_model::walk(v, &mut |_, _| n += 1); deckcraft_model::walk(v, &mut |_, _| n += 1);
n n
} }

View File

@ -1,7 +1,7 @@
//! File menu: new, open, save, export, close, properties. //! File menu: new, open, save, export, close, properties.
use deckcraft_model::{Presentation, defaults};
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_model::{Presentation, defaults};
use super::*; use super::*;
use crate::{DocState, EngineError, Result, Session}; use crate::{DocState, EngineError, Result, Session};
@ -11,11 +11,11 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!(noundo "file.new", "New Presentation", ["File"], Some("Cmd+N"), "{theme?: name, size?: [w,h] pt, blank?: bool}", always, new), 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.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.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.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!(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!(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|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!(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!(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!(noundo "file.close", "Close", ["File"], Some("Cmd+W"), "{}", has_doc, close),
cmd!( cmd!(
@ -36,8 +36,8 @@ pub fn specs() -> Vec<CommandSpec> {
] ]
} }
fn theme_named(name: &str) -> Option<slidecraft_model::Theme> { fn theme_named(name: &str) -> Option<deckcraft_model::Theme> {
slidecraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name)) deckcraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name))
} }
fn new(s: &mut Session, p: &Value) -> Result<Value> { fn new(s: &mut Session, p: &Value) -> Result<Value> {
@ -45,7 +45,7 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
let size = p let size = p
.get("size") .get("size")
.and_then(Value::as_array) .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) .filter(|s| s.width >= 1.0 && s.height >= 1.0 && s.width < 5000.0 && s.height < 5000.0)
.unwrap_or(defaults::WIDE); .unwrap_or(defaults::WIDE);
let doc = defaults::blank_presentation(size, theme, !bool_or(p, "blank", false)); let doc = defaults::blank_presentation(size, theme, !bool_or(p, "blank", false));
@ -56,25 +56,25 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
/// Recognise and read presentation bytes (native, PPTX). /// Recognise and read presentation bytes (native, PPTX).
pub fn open_presentation(name: &str, bytes: &[u8]) -> Result<Presentation> { pub fn open_presentation(name: &str, bytes: &[u8]) -> Result<Presentation> {
if slidecraft_format::sniff(bytes) { if deckcraft_format::sniff(bytes) {
return slidecraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string())); return deckcraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string()));
} }
if slidecraft_pptx::sniff(bytes) { if deckcraft_pptx::sniff(bytes) {
let mut p = slidecraft_pptx::import(bytes).map_err(|e| EngineError::Other(e.to_string()))?; let mut p = deckcraft_pptx::import(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
slidecraft_format::repair(&mut p); deckcraft_format::repair(&mut p);
return Ok(p); return Ok(p);
} }
let lower = name.to_ascii_lowercase(); let lower = name.to_ascii_lowercase();
if lower.ends_with(".txt") || lower.ends_with(".md") { if lower.ends_with(".txt") || lower.ends_with(".md") {
let text = String::from_utf8_lossy(bytes); let text = String::from_utf8_lossy(bytes);
let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false); 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() { if p.slides.is_empty() {
p = defaults::new_presentation(None); p = defaults::new_presentation(None);
} }
return Ok(p); 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<String>, p: Presentation) -> usize { pub fn add_opened(s: &mut Session, name: &str, path: Option<String>, p: Presentation) -> usize {
@ -87,7 +87,7 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_param(p, "path").ok_or_else(|| bad("file.open", "missing `path`"))?; 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 bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let doc = open_presentation(path, &bytes)?; 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); let i = add_opened(s, path, native.then(|| path.to_string()), doc);
Ok(json!({"document": i, "slides": s.doc()?.doc.slides.len()})) Ok(json!({"document": i, "slides": s.doc()?.doc.slides.len()}))
} }
@ -104,24 +104,24 @@ fn open_bytes(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"document": i})) 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<Vec<u8>> { pub fn save_bytes(doc: &Presentation, format: &str) -> Result<Vec<u8>> {
match format { match format {
"pptx" | "potx" | "ppsx" => slidecraft_pptx::export(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(slidecraft_format::slides_to_outline(doc).into_bytes()), "outline" | "txt" => Ok(deckcraft_format::slides_to_outline(doc).into_bytes()),
"pdf" => slidecraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())), "pdf" => deckcraft_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())), _ => 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], /// PDF options from command params: `{layout: slides|notes|handouts, perPage, dpi, slides: [i],
/// includeHidden, textLayer, frame}`. /// includeHidden, textLayer, frame}`.
pub fn pdf_options(p: &Value) -> slidecraft_pdf::PdfOptions { pub fn pdf_options(p: &Value) -> deckcraft_pdf::PdfOptions {
let mut o = slidecraft_pdf::PdfOptions { layout: slidecraft_pdf::PageLayout::Slides, ..Default::default() }; let mut o = deckcraft_pdf::PdfOptions { layout: deckcraft_pdf::PageLayout::Slides, ..Default::default() };
o.layout = match str_param(p, "layout").unwrap_or("slides") { o.layout = match str_param(p, "layout").unwrap_or("slides") {
"notes" => slidecraft_pdf::PageLayout::Notes, "notes" => deckcraft_pdf::PageLayout::Notes,
"handouts" => slidecraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).min(9) as u8 }, "handouts" => deckcraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).min(9) as u8 },
_ => slidecraft_pdf::PageLayout::Slides, _ => deckcraft_pdf::PageLayout::Slides,
}; };
o.dpi = f64_or(p, "dpi", o.dpi).clamp(36.0, 600.0); 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()); 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`. /// PDF bytes for `doc` with [`pdf_options`] from `p`.
pub fn pdf_bytes(doc: &Presentation, p: &Value) -> Result<Vec<u8>> { pub fn pdf_bytes(doc: &Presentation, p: &Value) -> Result<Vec<u8>> {
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 { 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") { } else if l.ends_with(".txt") || l.ends_with(".md") {
"outline" "outline"
} else { } else {
"slidecraft" "deckcraft"
} }
} }
@ -201,7 +201,7 @@ fn save_as(s: &mut Session, p: &Value) -> Result<Value> {
} }
fn save_bytes_cmd(s: &mut Session, p: &Value) -> Result<Value> { fn save_bytes_cmd(s: &mut Session, p: &Value) -> Result<Value> {
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)?; let bytes = save_bytes(&s.doc()?.doc, format)?;
Ok(json!({"data": base64_encode(&bytes), "bytes": bytes.len()})) Ok(json!({"data": base64_encode(&bytes), "bytes": bytes.len()}))
} }
@ -209,7 +209,7 @@ fn save_bytes_cmd(s: &mut Session, p: &Value) -> Result<Value> {
/// Render slide `index` (current when absent) to PNG bytes. /// Render slide `index` (current when absent) to PNG bytes.
pub fn render_png(doc: &Presentation, index: usize, scale: f64, edit: bool) -> (Vec<u8>, u32, u32) { pub fn render_png(doc: &Presentation, index: usize, scale: f64, edit: bool) -> (Vec<u8>, u32, u32) {
let img = 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) (img.to_png(), img.width, img.height)
} }
@ -240,10 +240,10 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
if *i >= st.doc.slides.len() { if *i >= st.doc.slides.len() {
return Err(bad("file.export", format!("no slide {i}"))); return Err(bad("file.export", format!("no slide {i}")));
} }
let img = slidecraft_render::render_slide( let img = deckcraft_render::render_slide(
&st.doc, &st.doc,
*i, *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 bytes = if format == "png" { img.to_png() } else { img.to_jpeg(92) };
let out = if slides.len() == 1 { let out = if slides.len() == 1 {
@ -260,7 +260,7 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"files": written})) Ok(json!({"files": written}))
} }
"pdf" => { "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)?; write_file(&path, &bytes)?;
Ok(json!({"path": path, "bytes": bytes.len()})) Ok(json!({"path": path, "bytes": bytes.len()}))
} }

View File

@ -1,11 +1,11 @@
//! Home ▸ Font and Paragraph, Format menu: character, paragraph and text box formatting, and //! Home ▸ Font and Paragraph, Format menu: character, paragraph and text box formatting, and
//! the Format Painter. //! 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 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::text::{format_body, format_para, format_run};
use super::*; use super::*;
@ -289,8 +289,8 @@ fn clear(s: &mut Session, _p: &Value) -> Result<Value> {
} }
fn text_shadow(s: &mut Session, p: &Value) -> Result<Value> { fn text_shadow(s: &mut Session, p: &Value) -> Result<Value> {
let v = on_param(p, Some(current_run(s).shadow.is_some())); let v = on_param(p, Some(current_run(s).shadow.is_some()));
let sh = slidecraft_model::style::Shadow { let sh = deckcraft_model::style::Shadow {
color: slidecraft_model::ColorRef::rgb(slidecraft_color::Rgba::BLACK).with(slidecraft_color::ColorTransform::Alpha(43000)), color: deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::BLACK).with(deckcraft_color::ColorTransform::Alpha(43000)),
blur: 3.0, blur: 3.0,
dist: 2.0, dist: 2.0,
dir: 45.0, dir: 45.0,
@ -328,7 +328,7 @@ fn case(s: &mut Session, p: &Value) -> Result<Value> {
let mut pos = 0; let mut pos = 0;
for r in &mut para.runs { for r in &mut para.runs {
let len = r.char_len(); 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 rs = from.max(pos).saturating_sub(pos);
let re = to.min(pos + len).saturating_sub(pos); let re = to.min(pos + len).saturating_sub(pos);
if re > rs { if re > rs {
@ -349,7 +349,7 @@ fn case(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(t) = sh.text.as_mut() { if let Some(t) = sh.text.as_mut() {
for para in &mut t.paragraphs { for para in &mut t.paragraphs {
for r in &mut para.runs { 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); r.text = ed::change_case(&r.text, &mode);
} }
} }
@ -541,7 +541,7 @@ pub struct Painted {
pub fill: Option<Fill>, pub fill: Option<Fill>,
pub line: Option<Line>, pub line: Option<Line>,
pub effects: Option<Effects>, pub effects: Option<Effects>,
pub style: Option<slidecraft_model::ShapeStyle>, pub style: Option<deckcraft_model::ShapeStyle>,
} }
fn painter_pick(s: &mut Session, p: &Value) -> Result<Value> { fn painter_pick(s: &mut Session, p: &Value) -> Result<Value> {

View File

@ -1,12 +1,12 @@
//! Insert tab: shapes, text boxes, pictures, tables, charts, audio, video, WordArt, fields. //! 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 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 super::*;
use crate::{EngineError, Result, Session}; use crate::{EngineError, Result, Session};
@ -92,7 +92,7 @@ pub fn specs() -> Vec<CommandSpec> {
/// Default size for a click-inserted shape (1 inch square, PowerPoint style). /// Default size for a click-inserted shape (1 inch square, PowerPoint style).
fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm { 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) 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 _ => shape
.geom .geom
.preset_name() .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()) .map(|i| i.label.split(':').next().unwrap_or(i.label).trim().to_string())
.unwrap_or_else(|| "Shape".into()), .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<Value> { fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
let preset = str_param(p, "preset").unwrap_or("rect"); 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 info = deckcraft_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 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 rect = rect_param(p, "rect").unwrap_or_else(|| default_rect(s, 72.0, if line_like { 0.0 } else { 72.0 }));
let adj: Vec<f64> = p.get("adj").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default(); let adj: Vec<f64> = 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 { let mut shape = Shape {
@ -153,14 +153,14 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
} }
if let Some(tb) = shape.text.as_mut() { if let Some(tb) = shape.text.as_mut() {
for para in &mut tb.paragraphs { 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 { } else {
shape.kind = if preset.contains("Connector") { ShapeKind::Connector { start: None, end: None } } else { ShapeKind::Shape }; shape.kind = if preset.contains("Connector") { ShapeKind::Connector { start: None, end: None } } else { ShapeKind::Shape };
if preset == "straightConnector1" { if preset == "straightConnector1" {
shape.line = Some(slidecraft_model::Line { shape.line = Some(deckcraft_model::Line {
tail: Some(slidecraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() }), tail: Some(deckcraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() }),
..Default::default() ..Default::default()
}); });
} }
@ -174,7 +174,7 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
fn presets(_s: &mut Session, _p: &Value) -> Result<Value> { fn presets(_s: &mut Session, _p: &Value) -> Result<Value> {
Ok(Value::Array( 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()) .and_then(|r| r.into_dimensions().ok())
.map(|(w, h)| (w as f64 * 0.75, h as f64 * 0.75)) .map(|(w, h)| (w as f64 * 0.75, h as f64 * 0.75))
.unwrap_or((288.0, 216.0)); .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); 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) (w * k, h * k)
} }
@ -298,7 +298,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
if !ct.starts_with("image/") { if !ct.starts_with("image/") {
return Err(bad("insert.picture", format!("{name} is not a picture"))); 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"))); return Err(EngineError::Other(format!("{name}: the picture can't be read")));
} }
let (w, h) = fitted(s, &bytes); let (w, h) = fitted(s, &bytes);
@ -308,7 +308,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
.into_iter() .into_iter()
.find(|x| { .find(|x| {
x.ph_type() 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()) && x.text.as_ref().is_none_or(|t| t.is_empty())
}) })
.map(|x| (x.id, xfrm_of(&d.doc, &d.selection, x))) .map(|x| (x.id, xfrm_of(&d.doc, &d.selection, x)))
@ -349,7 +349,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
sh.kind = shape.kind.clone(); sh.kind = shape.kind.clone();
sh.xfrm = Some(rect); sh.xfrm = Some(rect);
sh.ph = phv.map(|mut p| { sh.ph = phv.map(|mut p| {
p.kind = slidecraft_model::PhType::Picture; p.kind = deckcraft_model::PhType::Picture;
p p
}); });
sh.text = None; sh.text = None;
@ -426,7 +426,7 @@ fn chart(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(series) = p.get("series").and_then(Value::as_array) { if let Some(series) = p.get("series").and_then(Value::as_array) {
c.series = series c.series = series
.iter() .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(), 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(), values: sv.get("values").and_then(Value::as_array).map(|a| a.iter().map(Value::as_f64).collect()).unwrap_or_default(),
..Default::default() ..Default::default()
@ -447,7 +447,7 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result<Value> {
let (name, bytes) = media_bytes(p, cmd)?; let (name, bytes) = media_bytes(p, cmd)?;
let ct = content_type(&name, &bytes); let ct = content_type(&name, &bytes);
let bytes = std::sync::Arc::new(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_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()); let has_audio = info.as_ref().is_ok_and(|i| i.audio.is_some());
match &info { match &info {
@ -521,7 +521,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
let mut shape = new_text_box(Xfrm::new(size.width * 0.2, size.height * 0.4, size.width * 0.6, 80.0), text); 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() { if let Some(t) = shape.text.as_mut() {
for para in &mut t.paragraphs { 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 { for r in &mut para.runs {
r.props.size = Some(54.0); r.props.size = Some(54.0);
r.props.bold = Some(style % 2 == 1); r.props.bold = Some(style % 2 == 1);
@ -532,7 +532,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
_ => SchemeSlot::Accent4, _ => SchemeSlot::Accent4,
}))); })));
if style % 3 == 2 { 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<Value> {
} }
fn hyperlink(s: &mut Session, p: &Value) -> Result<Value> { fn hyperlink(s: &mut Session, p: &Value) -> Result<Value> {
use slidecraft_model::text::{Action, Hyperlink}; use deckcraft_model::text::{Action, Hyperlink};
let action = if let Some(u) = str_param(p, "url") { let action = if let Some(u) = str_param(p, "url") {
Action::Url { url: u.to_string() } Action::Url { url: u.to_string() }
} else if let Some(i) = usize_param(p, "slide") { } else if let Some(i) = usize_param(p, "slide") {
@ -589,7 +589,7 @@ fn hyperlink(s: &mut Session, p: &Value) -> Result<Value> {
} }
fn action_button(s: &mut Session, p: &Value) -> Result<Value> { fn action_button(s: &mut Session, p: &Value) -> Result<Value> {
use slidecraft_model::text::{Action, Hyperlink}; use deckcraft_model::text::{Action, Hyperlink};
let kind = str_param(p, "kind").unwrap_or("custom"); let kind = str_param(p, "kind").unwrap_or("custom");
let (preset, action) = match kind { let (preset, action) = match kind {
"back" | "previous" => ("actionButtonBackPrevious", Some(Action::PreviousSlide)), "back" | "previous" => ("actionButtonBackPrevious", Some(Action::PreviousSlide)),
@ -644,9 +644,9 @@ fn chart_data(s: &mut Session, p: &Value) -> Result<Value> {
.get("categories") .get("categories")
.and_then(Value::as_array) .and_then(Value::as_array)
.map(|a| a.iter().map(|v| v.as_str().map(String::from).unwrap_or_else(|| v.to_string())).collect()); .map(|a| a.iter().map(|v| v.as_str().map(String::from).unwrap_or_else(|| v.to_string())).collect());
let series: Option<Vec<slidecraft_model::chart::Series>> = p.get("series").and_then(Value::as_array).map(|a| { let series: Option<Vec<deckcraft_model::chart::Series>> = p.get("series").and_then(Value::as_array).map(|a| {
a.iter() 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(), 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(), values: sv.get("values").and_then(Value::as_array).map(|v| v.iter().map(Value::as_f64).collect()).unwrap_or_default(),
..Default::default() ..Default::default()
@ -773,7 +773,7 @@ fn smart_art(s: &mut Session, p: &Value) -> Result<Value> {
let mut body = TextBody::from_text(items.get(i).map(String::as_str).unwrap_or("")); 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 }); body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
for para in &mut body.paragraphs { 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 { for r in &mut para.runs {
r.props.size = Some(20.0); r.props.size = Some(20.0);
} }

View File

@ -1,8 +1,8 @@
//! Inspection for agents: the deck, slides and shapes as JSON (verify work without screenshots). //! 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 serde_json::{Value, json};
use slidecraft_model::resolve::{self, Ctx};
use slidecraft_model::{Shape, ShapeKind};
use super::*; use super::*;
use crate::{Result, Session}; 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 fill = ctx.map(|c| {
let (f, ph) = resolve::fill(c, sh); let (f, ph) = resolve::fill(c, sh);
match f { match f {
Some(slidecraft_model::Fill::Solid { color }) => json!(format!("#{}", c.color(&color, ph).hex())), Some(deckcraft_model::Fill::Solid { color }) => json!(format!("#{}", c.color(&color, ph).hex())),
Some(slidecraft_model::Fill::None) | None => json!("none"), 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()), 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<Value> {
let ctx = Ctx::for_slide(&st.doc, sl); let ctx = Ctx::for_slide(&st.doc, sl);
let shapes: Vec<Value> = sl.shapes.iter().map(|sh| shape_summary(ctx.as_ref(), sh, 0)).collect(); let shapes: Vec<Value> = 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)) { 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(), (f, _) => serde_json::to_value(&f).ok().and_then(|v| v.get("kind").cloned()).unwrap_or_default(),
}); });
Ok(json!({ Ok(json!({

View File

@ -2,8 +2,8 @@
//! requests the UI host carries out with its player ([`crate::UiRequest::Media`]); the host reports //! 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. //! back through [`Session::media_status`], which `media.info` returns with the probe.
use deckcraft_model::{MediaClip, ShapeId, ShapeKind};
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_model::{MediaClip, ShapeId, ShapeKind};
use super::*; use super::*;
use crate::{EngineError, MediaStatus, Result, Session, UiRequest}; use crate::{EngineError, MediaStatus, Result, Session, UiRequest};
@ -83,7 +83,7 @@ fn seek(s: &mut Session, p: &Value) -> Result<Value> {
request(s, p, "media.seek", "seek", Some(ms)) 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!({ json!({
"container": info.container, "container": info.container,
"durationMs": info.duration_ms, "durationMs": info.duration_ms,
@ -97,7 +97,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
let (id, m) = target(s, p, "media.info")?; let (id, m) = target(s, p, "media.info")?;
let st = s.doc()?; let st = s.doc()?;
let item = st.doc.media(m.media).ok_or_else(|| EngineError::Other("the media data is missing".into()))?; 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!({ Ok(json!({
"id": id, "id": id,
"name": item.name, "name": item.name,
@ -119,8 +119,8 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
} }
/// A poster frame PNG for video bytes at `ms`. /// A poster frame PNG for video bytes at `ms`.
pub(crate) fn poster_png(bytes: &slidecraft_media::Bytes, ms: u64) -> Option<Vec<u8>> { pub(crate) fn poster_png(bytes: &deckcraft_media::Bytes, ms: u64) -> Option<Vec<u8>> {
match slidecraft_media::poster_png(bytes, ms as f64 / 1000.0) { match deckcraft_media::poster_png(bytes, ms as f64 / 1000.0) {
Ok(png) => Some(png), Ok(png) => Some(png),
Err(e) => { Err(e) => {
log::info!("no poster frame: {e}"); log::info!("no poster frame: {e}");

View File

@ -2,12 +2,12 @@
//! the outlines of the selected shapes. The result takes the formatting of the first selected //! the outlines of the selected shapes. The result takes the formatting of the first selected
//! shape, like PowerPoint. //! 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::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule; use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay; use i_overlay::float::single::SingleFloatOverlay;
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_geom::{PathEl, Rect, Xfrm};
use slidecraft_model::{CustomPath, Geom, Shape, ShapeId, ShapeKind};
use super::*; use super::*;
use crate::{Result, Session}; use crate::{Result, Session};
@ -31,12 +31,12 @@ pub fn specs() -> Vec<CommandSpec> {
} }
/// The filled outline of `s` in slide points (curves flattened). /// 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 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 a = x.affine();
let mut out: Area = vec![]; 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 mut cur: Vec<[f64; 2]> = vec![];
let flush = |cur: &mut Vec<[f64; 2]>, out: &mut Area| { let flush = |cur: &mut Vec<[f64; 2]>, out: &mut Area| {
if cur.len() > 2 { if cur.len() > 2 {
@ -45,7 +45,7 @@ fn area_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, s: &Sha
cur.clear(); 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) => { PathEl::MoveTo(p) => {
flush(&mut cur, &mut out); flush(&mut cur, &mut out);
let q = a * p; let q = a * p;

View File

@ -17,10 +17,10 @@ pub mod table;
pub mod text; pub mod text;
pub mod transition; pub mod transition;
use deckcraft_geom::Xfrm;
use deckcraft_model::ShapeId;
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use slidecraft_geom::Xfrm;
use slidecraft_model::ShapeId;
use crate::{EngineError, Result, Session}; use crate::{EngineError, Result, Session};
@ -207,31 +207,31 @@ pub(crate) fn ok() -> Result<Value> {
} }
/// Parse a colour param: `"#RRGGBB"`, `"accent1"`…, or `{"scheme":"accent2","lumMod":75000}`. /// Parse a colour param: `"#RRGGBB"`, `"accent1"`…, or `{"scheme":"accent2","lumMod":75000}`.
pub(crate) fn color_param(p: &Value, key: &str) -> Option<slidecraft_model::ColorRef> { pub(crate) fn color_param(p: &Value, key: &str) -> Option<deckcraft_model::ColorRef> {
let v = p.get(key)?; let v = p.get(key)?;
color_value(v) color_value(v)
} }
pub(crate) fn color_value(v: &Value) -> Option<slidecraft_model::ColorRef> { pub(crate) fn color_value(v: &Value) -> Option<deckcraft_model::ColorRef> {
use slidecraft_model::ColorRef; use deckcraft_model::ColorRef;
match v { match v {
Value::String(s) => { 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)); 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)); return Some(ColorRef::rgb(c));
} }
slidecraft_color::preset(s).map(ColorRef::rgb) deckcraft_color::preset(s).map(ColorRef::rgb)
} }
Value::Object(o) => { Value::Object(o) => {
let mut c = if let Some(s) = o.get("scheme").and_then(Value::as_str) { 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 { } 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 { 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); c.mods.push(t);
} }
} }
@ -242,20 +242,20 @@ pub(crate) fn color_value(v: &Value) -> Option<slidecraft_model::ColorRef> {
} }
/// Look a shape up by id in the edited tree. /// Look a shape up by id in the edited tree.
pub(crate) fn shape_of(s: &Session, id: ShapeId) -> Result<slidecraft_model::Shape> { pub(crate) fn shape_of(s: &Session, id: ShapeId) -> Result<deckcraft_model::Shape> {
s.doc()?.shape(id).cloned().ok_or_else(|| EngineError::Other(format!("no shape {id}"))) s.doc()?.shape(id).cloned().ok_or_else(|| EngineError::Other(format!("no shape {id}")))
} }
/// Effective box of a shape (placeholders inherit theirs). /// 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 { if let Some(x) = shape.xfrm {
return x; return x;
} }
ctx_xfrm(doc, sel, shape) ctx_xfrm(doc, sel, shape)
} }
fn ctx_xfrm(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape: &slidecraft_model::Shape) -> Xfrm { fn ctx_xfrm(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm {
use slidecraft_model::resolve::{self, Ctx}; use deckcraft_model::resolve::{self, Ctx};
match sel.target { 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::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)), 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. /// 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<Value> { pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut deckcraft_model::Shape) -> Result<()>) -> Result<Value> {
let ids = targets(s, p)?; let ids = targets(s, p)?;
if ids.is_empty() { if ids.is_empty() {
return Err(bad(cmd, "no shapes selected")); return Err(bad(cmd, "no shapes selected"));
} }
s.edit(|doc, sel| { s.edit(|doc, sel| {
let boxes: Vec<(ShapeId, Xfrm)> = { let boxes: Vec<(ShapeId, Xfrm)> = {
let st_shapes: Vec<slidecraft_model::Shape> = let st_shapes: Vec<deckcraft_model::Shape> =
ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| slidecraft_model::find_shape(v, *id)).cloned()).collect(); 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() 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 shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
let mut n = 0; let mut n = 0;
for id in &ids { 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() { if sh.xfrm.is_none() {
sh.xfrm = boxes.iter().find(|(i, _)| i == id).map(|(_, x)| *x); 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<slidecraft_model::Shape>> { pub(crate) fn current_shapes<'a>(doc: &'a deckcraft_model::Presentation, sel: &crate::Selection) -> Option<&'a Vec<deckcraft_model::Shape>> {
match sel.target { match sel.target {
crate::Target::Slides => doc.slides.get(sel.slide).map(|s| &s.shapes), crate::Target::Slides => doc.slides.get(sel.slide).map(|s| &s.shapes),
crate::Target::Master { master } => doc.masters.get(master).map(|m| &m.shapes), crate::Target::Master { master } => doc.masters.get(master).map(|m| &m.shapes),

View File

@ -2,8 +2,8 @@
use std::sync::Arc; use std::sync::Arc;
use deckcraft_model::Comment;
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_model::Comment;
use super::*; use super::*;
use crate::{Result, Session, UiRequest}; use crate::{Result, Session, UiRequest};
@ -130,34 +130,34 @@ fn accessibility(s: &mut Session, _p: &Value) -> Result<Value> {
if sl.title().trim().is_empty() { if sl.title().trim().is_empty() {
out.push(json!({"slide": i, "issue": "Missing slide title"})); 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!( let needs_alt = matches!(
sh.kind, sh.kind,
slidecraft_model::ShapeKind::Picture { .. } deckcraft_model::ShapeKind::Picture { .. }
| slidecraft_model::ShapeKind::Chart(_) | deckcraft_model::ShapeKind::Chart(_)
| slidecraft_model::ShapeKind::Media(_) | deckcraft_model::ShapeKind::Media(_)
| slidecraft_model::ShapeKind::Group { .. } | deckcraft_model::ShapeKind::Group { .. }
); );
if needs_alt && sh.descr.trim().is_empty() && !sh.decorative { 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"})); out.push(json!({"slide": i, "shape": sh.id, "name": sh.name, "issue": "Missing alternative text"}));
} }
}); });
// Low-contrast text against the slide background. // Low-contrast text against the slide background.
if let Some(ctx) = slidecraft_model::resolve::Ctx::for_slide(&st.doc, sl) { if let Some(ctx) = deckcraft_model::resolve::Ctx::for_slide(&st.doc, sl) {
let bg = match slidecraft_model::resolve::background(&ctx, Some(sl)) { let bg = match deckcraft_model::resolve::background(&ctx, Some(sl)) {
(slidecraft_model::Fill::Solid { color }, ph) => Some(ctx.color(&color, ph)), (deckcraft_model::Fill::Solid { color }, ph) => Some(ctx.color(&color, ph)),
_ => None, _ => None,
}; };
if let Some(bg) = bg { if let Some(bg) = bg {
for sh in &sl.shapes { for sh in &sl.shapes {
let Some(t) = &sh.text else { continue }; let Some(t) = &sh.text else { continue };
let Some(para) = t.paragraphs.iter().find(|p| !p.is_empty()) else { continue }; let Some(para) = t.paragraphs.iter().find(|p| !p.is_empty()) else { continue };
let rp = slidecraft_model::resolve::run(&ctx, sh, para, &para.runs.first().map(|r| r.props.clone()).unwrap_or_default()); let rp = deckcraft_model::resolve::run(&ctx, sh, para, &para.runs.first().map(|r| r.props.clone()).unwrap_or_default());
let (fill, _) = slidecraft_model::resolve::fill(&ctx, sh); let (fill, _) = deckcraft_model::resolve::fill(&ctx, sh);
if fill.as_ref().is_some_and(|f| !f.is_none()) { if fill.as_ref().is_some_and(|f| !f.is_none()) {
continue; 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 { 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))})); 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<Value> {
let st = s.doc()?; let st = s.doc()?;
let mut out = vec![]; let mut out = vec![];
for (i, sl) in st.doc.slides.iter().enumerate() { 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 { if let Some(t) = &sh.text {
for w in t.text().split(|c: char| !c.is_alphanumeric() && c != '\'') { for w in t.text().split(|c: char| !c.is_alphanumeric() && c != '\'') {
if COMMON.contains(&w.to_lowercase().as_str()) { if COMMON.contains(&w.to_lowercase().as_str()) {
@ -258,7 +258,7 @@ fn custom_show(s: &mut Session, p: &Value) -> Result<Value> {
doc.custom_shows.retain(|c| c.name != name); doc.custom_shows.retain(|c| c.name != name);
if !delete { if !delete {
let slides = idx.iter().filter_map(|i| doc.slides.get(*i).map(|x| x.id)).collect(); 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(()) Ok(())
})?; })?;

View File

@ -1,10 +1,10 @@
//! Shape Format: position and size, rotation, flips, geometry, fill, outline, effects, styles, //! Shape Format: position and size, rotation, flips, geometry, fill, outline, effects, styles,
//! names and alt text. //! 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 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 super::*;
use crate::{Result, Session}; use crate::{Result, Session};
@ -195,7 +195,7 @@ fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
let v = f64_param(p, "value").ok_or_else(|| bad("shape.adjust", "missing `value`"))?; let v = f64_param(p, "value").ok_or_else(|| bad("shape.adjust", "missing `value`"))?;
edit_shapes(s, p, "shape.adjust", |sh| { edit_shapes(s, p, "shape.adjust", |sh| {
if let Geom::Preset { name, adj } = &mut sh.geom { 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 { while adj.len() <= i {
let k = adj.len(); let k = adj.len();
adj.push(defaults.get(k).copied().unwrap_or(0.0)); adj.push(defaults.get(k).copied().unwrap_or(0.0));
@ -210,7 +210,7 @@ fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
fn change(s: &mut Session, p: &Value) -> Result<Value> { fn change(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "preset").ok_or_else(|| bad("shape.change", "missing `preset`"))?; 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| { edit_shapes(s, p, "shape.change", |sh| {
sh.geom = Geom::preset(info.name); sh.geom = Geom::preset(info.name);
Ok(()) Ok(())
@ -287,7 +287,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
} }
// Transparency on an inherited (style) fill needs the effective colour. // Transparency on an inherited (style) fill needs the effective colour.
let st = s.doc()?; let st = s.doc()?;
let ctx_fill: Vec<(slidecraft_model::ShapeId, Option<Fill>)> = { let ctx_fill: Vec<(deckcraft_model::ShapeId, Option<Fill>)> = {
let ctx = super::text::ctx_for(&st.doc, &st.selection); let ctx = super::text::ctx_for(&st.doc, &st.selection);
targets(s, p)? targets(s, p)?
.into_iter() .into_iter()
@ -296,7 +296,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
( (
id, id,
ctx.as_ref().and_then(|c| { 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 { f.map(|f| match f {
Fill::Solid { color } => Fill::solid(ColorRef::rgb(c.color(&color, ph))), Fill::Solid { color } => Fill::solid(ColorRef::rgb(c.color(&color, ph))),
other => other, other => other,
@ -338,14 +338,14 @@ fn line(s: &mut Session, p: &Value) -> Result<Value> {
let head = str_param(p, "head").map(line_end); let head = str_param(p, "head").map(line_end);
let tail = str_param(p, "tail").map(line_end); let tail = str_param(p, "tail").map(line_end);
let cap = str_param(p, "cap").map(|c| match c { let cap = str_param(p, "cap").map(|c| match c {
"round" => slidecraft_model::style::LineCap::Round, "round" => deckcraft_model::style::LineCap::Round,
"square" => slidecraft_model::style::LineCap::Square, "square" => deckcraft_model::style::LineCap::Square,
_ => slidecraft_model::style::LineCap::Flat, _ => deckcraft_model::style::LineCap::Flat,
}); });
let join = str_param(p, "join").map(|c| match c { let join = str_param(p, "join").map(|c| match c {
"bevel" => slidecraft_model::style::LineJoin::Bevel, "bevel" => deckcraft_model::style::LineJoin::Bevel,
"miter" => slidecraft_model::style::LineJoin::Miter, "miter" => deckcraft_model::style::LineJoin::Miter,
_ => slidecraft_model::style::LineJoin::Round, _ => deckcraft_model::style::LineJoin::Round,
}); });
edit_shapes(s, p, "shape.line", |sh| { edit_shapes(s, p, "shape.line", |sh| {
if reset { if reset {
@ -536,7 +536,7 @@ fn quick_style(s: &mut Session, p: &Value) -> Result<Value> {
sh.line.clone_from(&l); sh.line.clone_from(&l);
sh.effects = None; sh.effects = None;
if let Some(t) = sh.text.as_mut() { 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(()) Ok(())
}) })
@ -596,7 +596,7 @@ fn set_default(s: &mut Session, _p: &Value) -> Result<Value> {
fn crop(s: &mut Session, p: &Value) -> Result<Value> { fn crop(s: &mut Session, p: &Value) -> Result<Value> {
let c = [f64_param(p, "left"), f64_param(p, "top"), f64_param(p, "right"), f64_param(p, "bottom")]; 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| { 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() { for (i, v) in c.iter().enumerate() {
if let (Some(v), Some(slot)) = (v, fill.crop.get_mut(i)) { if let (Some(v), Some(slot)) = (v, fill.crop.get_mut(i)) {
*slot = v.clamp(-10.0, 0.95); *slot = v.clamp(-10.0, 0.95);
@ -609,7 +609,7 @@ fn crop(s: &mut Session, p: &Value) -> Result<Value> {
fn picture_adjust(s: &mut Session, p: &Value) -> Result<Value> { fn picture_adjust(s: &mut Session, p: &Value) -> Result<Value> {
edit_shapes(s, p, "picture.adjust", |sh| { 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; let a = &mut fill.adjust;
if bool_or(p, "reset", false) { if bool_or(p, "reset", false) {
*a = Default::default(); *a = Default::default();
@ -636,7 +636,7 @@ fn picture_adjust(s: &mut Session, p: &Value) -> Result<Value> {
fn picture_reset(s: &mut Session, p: &Value) -> Result<Value> { fn picture_reset(s: &mut Session, p: &Value) -> Result<Value> {
edit_shapes(s, p, "picture.reset", |sh| { 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.adjust = Default::default();
fill.crop = [0.0; 4]; fill.crop = [0.0; 4];
fill.alpha = None; fill.alpha = None;
@ -652,7 +652,7 @@ fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
let ct = super::insert::content_type(&name, &bytes); let ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?; let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
edit_shapes(s, p, "picture.change", |sh| { 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.media = media;
fill.crop = [0.0; 4]; fill.crop = [0.0; 4];
} }
@ -662,7 +662,7 @@ fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
fn media_options(s: &mut Session, p: &Value) -> Result<Value> { fn media_options(s: &mut Session, p: &Value) -> Result<Value> {
edit_shapes(s, p, "media.options", |sh| { 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") { if let Some(v) = bool_param(p, "autoplay") {
m.autoplay = v; m.autoplay = v;
} }
@ -703,8 +703,8 @@ fn media_options(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({})) Ok(json!({}))
} }
fn shape_id(p: &Value, key: &str) -> Option<slidecraft_model::ShapeId> { fn shape_id(p: &Value, key: &str) -> Option<deckcraft_model::ShapeId> {
p.get(key).and_then(Value::as_u64).map(|v| slidecraft_model::ShapeId(v as u32)) p.get(key).and_then(Value::as_u64).map(|v| deckcraft_model::ShapeId(v as u32))
} }
fn sites(s: &mut Session, p: &Value) -> Result<Value> { fn sites(s: &mut Session, p: &Value) -> Result<Value> {
@ -722,7 +722,7 @@ fn connect(s: &mut Session, p: &Value) -> Result<Value> {
if a.is_empty() || b.is_empty() { if a.is_empty() || b.is_empty() {
return Err(bad("shape.connect", "both shapes need connection sites (lines and groups have none)")); 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")) { let (ia, ib) = match (pick(&a, "fromSite"), pick(&b, "toSite")) {
(Some(i), Some(j)) => (i, j), (Some(i), Some(j)) => (i, j),
(fi, fj) => { (fi, fj) => {
@ -744,24 +744,24 @@ fn connect(s: &mut Session, p: &Value) -> Result<Value> {
None => { None => {
let preset = str_param(p, "preset").unwrap_or("straightConnector1"); 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]}))?; 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| { s.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.connect", "no slide"))?; 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() { if !sh.is_line() {
return Err(bad("shape.connect", "`id` is not a line or connector")); return Err(bad("shape.connect", "`id` is not a line or connector"));
} }
sh.xfrm = Some(crate::tools::line_xfrm(p0, p1, 0.0)); 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(())
})?; })?;
Ok(json!({"id": id.0, "fromSite": ia, "toSite": ib})) Ok(json!({"id": id.0, "fromSite": ia, "toSite": ib}))
} }
/// Path data through `pts` (shape-local): straight segments, or a Catmull-Rom curve as cubics. /// 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 f = |v: f64| {
let s = format!("{:.2}", v); let s = format!("{:.2}", v);
s.trim_end_matches('0').trim_end_matches('.').to_string() 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 mut d = format!("M {} {}", f(pts[0].x), f(pts[0].y));
let n = pts.len(); let n = pts.len();
if smooth && n > 2 { 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] } 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 }; 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<Value> { fn freeform(s: &mut Session, p: &Value) -> Result<Value> {
let pts: Vec<slidecraft_geom::Point> = p let pts: Vec<deckcraft_geom::Point> = p
.get("points") .get("points")
.and_then(Value::as_array) .and_then(Value::as_array)
.map(|a| { .map(|a| {
a.iter() 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()) .filter(|q| q.x.is_finite() && q.y.is_finite())
.take(20_000) .take(20_000)
.collect() .collect()
@ -807,30 +807,30 @@ fn freeform(s: &mut Session, p: &Value) -> Result<Value> {
} }
let closed = bool_or(p, "closed", false) && pts.len() > 2; let closed = bool_or(p, "closed", false) && pts.len() > 2;
let smooth = bool_or(p, "smooth", false); 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 { for q in &pts {
bb = bb.union_pt(*q); bb = bb.union_pt(*q);
} }
let (w, h) = (bb.width().max(1.0), bb.height().max(1.0)); let (w, h) = (bb.width().max(1.0), bb.height().max(1.0));
let local: Vec<slidecraft_geom::Point> = pts.iter().map(|q| slidecraft_geom::Point::new(q.x - bb.x0, q.y - bb.y0)).collect(); let local: Vec<deckcraft_geom::Point> = pts.iter().map(|q| deckcraft_geom::Point::new(q.x - bb.x0, q.y - bb.y0)).collect();
let d = freeform_path(&local, closed, smooth); let d = freeform_path(&local, closed, smooth);
// Closed shapes take the default shape style, open paths the line style. // 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 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| { s.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.freeform", "no slide"))?; 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 { sh.geom = Geom::Custom {
paths: vec![slidecraft_model::CustomPath { paths: vec![deckcraft_model::CustomPath {
w, w,
h, h,
d: d.clone(), 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, stroke: true,
}], }],
}; };
sh.xfrm = Some(slidecraft_geom::Xfrm::new(bb.x0, bb.y0, w, h)); sh.xfrm = Some(deckcraft_geom::Xfrm::new(bb.x0, bb.y0, w, h));
sh.kind = slidecraft_model::ShapeKind::Shape; sh.kind = deckcraft_model::ShapeKind::Shape;
sh.name = format!("Freeform {}", id.0); sh.name = format!("Freeform {}", id.0);
Ok(()) Ok(())
})?; })?;

View File

@ -2,9 +2,9 @@
use std::sync::Arc; use std::sync::Arc;
use deckcraft_model::text::TextBody;
use deckcraft_model::{LayoutId, LayoutType, Presentation, Section, SlideId, defaults};
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_model::text::TextBody;
use slidecraft_model::{LayoutId, LayoutType, Presentation, Section, SlideId, defaults};
use super::*; use super::*;
use crate::{Result, Session, Target}; use crate::{Result, Session, Target};
@ -173,12 +173,12 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
.get(*i) .get(*i)
.map(|x| { .map(|x| {
let mut v = vec![]; 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 v
}) })
.unwrap_or_default(); .unwrap_or_default();
let mut new_shape_ids = vec![]; let mut new_shape_ids = vec![];
slidecraft_model::walk(&copy.shapes, &mut |sh, _| new_shape_ids.push(sh.id)); deckcraft_model::walk(&copy.shapes, &mut |sh, _| new_shape_ids.push(sh.id));
for a in &mut copy.animations { for a in &mut copy.animations {
if let Some(pos) = old_ids.iter().position(|x| *x == a.shape) { if let Some(pos) = old_ids.iter().position(|x| *x == a.shape) {
a.shape = new_shape_ids.get(pos).copied().unwrap_or(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<Value> {
/// Re-map a slide's placeholders onto a new layout (content kept, matched by type/index). /// 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<()> { pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> Result<()> {
let lay_shapes: Vec<slidecraft_model::Shape> = let lay_shapes: Vec<deckcraft_model::Shape> =
doc.layout(layout).map(|(_, l)| l.shapes.clone()).ok_or_else(|| bad("slide.layout", "no such layout"))?; doc.layout(layout).map(|(_, l)| l.shapes.clone()).ok_or_else(|| bad("slide.layout", "no such layout"))?;
let mut fresh = vec![]; 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())) { 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())); fresh.push((id, ls.clone()));
} }
let slide = doc.slides.get_mut(index).ok_or_else(|| bad("slide.layout", "no slide"))?; let slide = doc.slides.get_mut(index).ok_or_else(|| bad("slide.layout", "no slide"))?;
let slide = Arc::make_mut(slide); let slide = Arc::make_mut(slide);
let mut old: Vec<slidecraft_model::Shape> = std::mem::take(&mut slide.shapes); let mut old: Vec<deckcraft_model::Shape> = std::mem::take(&mut slide.shapes);
let mut out = vec![]; let mut out = vec![];
for (id, ls) in fresh { for (id, ls) in fresh {
let Some(lph) = ls.ph.clone() else { continue }; 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); out.push(o);
} }
None => { 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. // Leftover placeholders with content stay as they were (positioned); empty ones go.
for o in old { 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 { if !empty_ph {
out.push(o); out.push(o);
} }
@ -574,7 +574,7 @@ fn from_outline(s: &mut Session, p: &Value) -> Result<Value> {
let text = str_param(p, "text").unwrap_or("").to_string(); let text = str_param(p, "text").unwrap_or("").to_string();
s.edit(|doc, sel| { s.edit(|doc, sel| {
let before = doc.slides.len(); 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. // outline_to_slides appends; move them after the current slide.
let at = (sel.slide + 1).min(before); let at = (sel.slide + 1).min(before);
let new: Vec<_> = doc.slides.drain(before..).collect(); let new: Vec<_> = doc.slides.drain(before..).collect();

View File

@ -1,7 +1,7 @@
//! Table Design and Table Layout tabs. //! Table Design and Table Layout tabs.
use deckcraft_model::{ShapeKind, Table};
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_model::{ShapeKind, Table};
use super::*; use super::*;
use crate::{Result, Session}; 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. // The edited cell may have moved out of range.
s.select(|doc, sel| { s.select(|doc, sel| {
let dims = super::current_shapes(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 }); .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) if let (Some(t), Some((nr, nc))) = (sel.text.as_mut(), dims)
&& let Some((r, c)) = t.cell && let Some((r, c)) = t.cell
@ -196,8 +196,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
} }
fn cell_fill(s: &mut Session, p: &Value) -> Result<Value> { fn cell_fill(s: &mut Session, p: &Value) -> Result<Value> {
let fill = let fill = if bool_or(p, "none", false) { Some(deckcraft_model::Fill::None) } else { color_param(p, "color").map(deckcraft_model::Fill::solid) };
if bool_or(p, "none", false) { Some(slidecraft_model::Fill::None) } else { color_param(p, "color").map(slidecraft_model::Fill::solid) };
let cells: Option<Vec<(usize, usize)>> = p let cells: Option<Vec<(usize, usize)>> = p
.get("cells") .get("cells")
.and_then(Value::as_array) .and_then(Value::as_array)

View File

@ -2,11 +2,11 @@
use std::sync::Arc; 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 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 super::*;
use crate::{DocState, Result, Selection, Session, Target, TextSel}; 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); return doc.slides.get_mut(sel.slide).map(|s| &mut Arc::make_mut(s).notes);
} }
let shapes = crate::shapes_mut(doc, sel)?; 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 Some((r, c)) = t.cell {
if let ShapeKind::Table(tb) = &mut sh.kind { if let ShapeKind::Table(tb) = &mut sh.kind {
return tb.cell_mut(r, c).map(|x| &mut x.text); 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(()); return Ok(());
} }
let x = xfrm_of(&doc, &sel, &sh); let x = xfrm_of(&doc, &sel, &sh);
let geo = slidecraft_render::shape_geometry(&sh, x.w, x.h); let geo = deckcraft_render::shape_geometry(&sh, x.w, x.h);
let fields = slidecraft_text::NoFields; let fields = deckcraft_text::NoFields;
let mut nx = x; let mut nx = x;
if bp.wrap == Some(false) { 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 geo = deckcraft_render::shape_geometry(&sh, nx.w, nx.h);
let need = slidecraft_text::fit_height(&ctx, &sh, &body, geo.text_rect, &fields); let need = deckcraft_text::fit_height(&ctx, &sh, &body, geo.text_rect, &fields);
let extra = (nx.h - geo.text_rect.height()).max(0.0); let extra = (nx.h - geo.text_rect.height()).max(0.0);
nx.h = (need + extra).max(8.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() { 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 st = s.doc_mut()?;
let mut d = (*st.doc).clone(); let mut d = (*st.doc).clone();
if let Some(list) = crate::shapes_mut(&mut d, &st.selection) 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); target.xfrm = Some(nx);
} }
@ -127,7 +127,7 @@ pub(crate) fn ctx_for<'a>(doc: &'a Presentation, sel: &Selection) -> Option<Ctx<
} }
/// Lay out the text being edited (for vertical caret movement and hit testing). /// Lay out the text being edited (for vertical caret movement and hit testing).
pub fn layout_for(st: &DocState, t: &TextSel) -> Option<slidecraft_text::TextLayout> { pub fn layout_for(st: &DocState, t: &TextSel) -> Option<deckcraft_text::TextLayout> {
if t.notes || t.cell.is_some() { if t.notes || t.cell.is_some() {
return None; return None;
} }
@ -135,12 +135,12 @@ pub fn layout_for(st: &DocState, t: &TextSel) -> Option<slidecraft_text::TextLay
let body = sh.text.as_ref()?; let body = sh.text.as_ref()?;
let ctx = ctx_for(&st.doc, &st.selection)?; let ctx = ctx_for(&st.doc, &st.selection)?;
let x = xfrm_of(&st.doc, &st.selection, sh); let x = xfrm_of(&st.doc, &st.selection, sh);
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h); let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
Some(slidecraft_text::layout( Some(deckcraft_text::layout(
&ctx, &ctx,
sh, sh,
body, body,
&slidecraft_text::Opts { rect: geo.text_rect, fields: &slidecraft_text::NoFields, prompt_color: None, no_shrink: false }, &deckcraft_text::Opts { rect: geo.text_rect, fields: &deckcraft_text::NoFields, prompt_color: None, no_shrink: false },
)) ))
} }
@ -175,7 +175,7 @@ fn edit(s: &mut Session, p: &Value) -> Result<Value> {
if sh.text.is_none() && cell.is_none() { if sh.text.is_none() && cell.is_none() {
s.edit(|doc, sel| { s.edit(|doc, sel| {
if let Some(list) = crate::shapes_mut(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()); x.text = Some(TextBody::default());
} }
@ -313,7 +313,7 @@ pub(crate) fn insert_field(s: &mut Session, field: &str, text: &str) -> Result<V
let at = ed::delete(body, a, b); let at = ed::delete(body, a, b);
let props = ed::props_at(body, at); let props = ed::props_at(body, at);
let src = TextBody { let src = TextBody {
paragraphs: vec![slidecraft_model::text::Paragraph { paragraphs: vec![deckcraft_model::text::Paragraph {
runs: vec![Run { text: text.clone(), props, kind: RunKind::Field { field: field.clone() } }], runs: vec![Run { text: text.clone(), props, kind: RunKind::Field { field: field.clone() } }],
..Default::default() ..Default::default()
}], }],
@ -375,7 +375,7 @@ fn move_caret(s: &mut Session, p: &Value) -> Result<Value> {
let t = st.selection.text.clone().ok_or_else(|| bad("text.move", "not editing text"))?; 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 body = body_of(st, &t).cloned().unwrap_or_default();
let layout = layout_for(st, &t); 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 caret = t.caret;
let collapse_to = |left: bool| if left { t.ordered().0 } else { t.ordered().1 }; let collapse_to = |left: bool| if left { t.ordered().0 } else { t.ordered().1 };
let new = match to.as_str() { let new = match to.as_str() {
@ -490,7 +490,7 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
}; };
let Some(body) = target else { return Err(bad("text.set", "no text here (tables need `cell`)")) }; 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 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); let mut nb = TextBody::from_text(&text);
for (i, para) in nb.paragraphs.iter_mut().enumerate() { for (i, para) in nb.paragraphs.iter_mut().enumerate() {
// Leading tabs set the outline level. // 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 /// Apply paragraph changes to the paragraphs touched by the selection (or all paragraphs of the
/// selected shapes). /// selected shapes).
pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::text::Paragraph) + Send + Sync)) -> Result<Value> { pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::text::Paragraph) + Send + Sync)) -> Result<Value> {
let st = s.doc()?; let st = s.doc()?;
if let Some(t) = st.selection.text.clone() { if let Some(t) = st.selection.text.clone() {
let (a, b) = t.ordered(); 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. /// 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<Value> { pub(crate) fn format_body(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::text::BodyProps) + Send + Sync)) -> Result<Value> {
let st = s.doc()?; let st = s.doc()?;
let ids: Vec<ShapeId> = match &st.selection.text { let ids: Vec<ShapeId> = match &st.selection.text {
Some(t) if !t.notes => vec![t.shape], Some(t) if !t.notes => vec![t.shape],

View File

@ -2,9 +2,9 @@
use std::sync::Arc; use std::sync::Arc;
use deckcraft_model::Transition;
use deckcraft_model::anim::TRANSITIONS;
use serde_json::{Value, json}; use serde_json::{Value, json};
use slidecraft_model::Transition;
use slidecraft_model::anim::TRANSITIONS;
use super::*; use super::*;
use crate::{Result, Session}; use crate::{Result, Session};

View File

@ -1,8 +1,8 @@
//! Connectors glued to shapes: connection sites, snapping line ends to them, and rerouting attached //! Connectors glued to shapes: connection sites, snapping line ends to them, and rerouting attached
//! connectors whenever the shapes they connect move, resize or rotate. //! connectors whenever the shapes they connect move, resize or rotate.
use slidecraft_geom::{Point, Xfrm}; use deckcraft_geom::{Point, Xfrm};
use slidecraft_model::{Presentation, Shape, ShapeId, ShapeKind}; use deckcraft_model::{Presentation, Shape, ShapeId, ShapeKind};
use crate::Selection; use crate::Selection;
use crate::cmd::xfrm_of; use crate::cmd::xfrm_of;
@ -13,7 +13,7 @@ pub fn sites(doc: &Presentation, sel: &Selection, s: &Shape) -> Vec<Point> {
return vec![]; return vec![];
} }
let x = xfrm_of(doc, sel, s); 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() { 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)]; 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)];
} }

View File

@ -176,7 +176,7 @@ impl Session {
let st = self.doc()?; let st = self.doc()?;
let Some(t) = st.selection.text.clone() else { return Ok(Value::Null) }; 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((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 (nr, nc) = (tb.n_rows(), tb.n_cols());
let idx = r * nc + c; let idx = r * nc + c;
if !back && idx + 1 >= nr * nc { if !back && idx + 1 >= nr * nc {

View File

@ -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`, //! 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 //! `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 std::sync::Arc;
use deckcraft_model::{Presentation, Shape, ShapeId, Slide, SlideId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use slidecraft_model::{Presentation, Shape, ShapeId, Slide, SlideId};
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command}; pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
pub use slidecraft_geom as geom; pub use deckcraft_geom as geom;
pub use slidecraft_media as media; pub use deckcraft_media as media;
pub use slidecraft_model as model; pub use deckcraft_model as model;
pub use slidecraft_render as render; pub use deckcraft_render as render;
pub use tools::{Mods, PointerEvent, PointerKind, ToolKind}; pub use tools::{Mods, PointerEvent, PointerKind, ToolKind};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -45,8 +45,8 @@ pub enum EngineError {
Internal(String, String), Internal(String, String),
} }
impl From<slidecraft_model::ModelError> for EngineError { impl From<deckcraft_model::ModelError> for EngineError {
fn from(e: slidecraft_model::ModelError) -> Self { fn from(e: deckcraft_model::ModelError) -> Self {
EngineError::Other(e.to_string()) EngineError::Other(e.to_string())
} }
} }
@ -184,7 +184,7 @@ impl DocState {
} }
} }
pub fn shape(&self, id: ShapeId) -> Option<&Shape> { 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> { pub fn selected_shapes(&self) -> Vec<&Shape> {
self.selection.shapes.iter().filter_map(|id| self.shape(*id)).collect() self.selection.shapes.iter().filter_map(|id| self.shape(*id)).collect()
@ -282,8 +282,8 @@ pub struct Clipboard {
pub shapes: Vec<Shape>, pub shapes: Vec<Shape>,
pub slides: Vec<Slide>, pub slides: Vec<Slide>,
/// Media the copied shapes/slides reference. /// Media the copied shapes/slides reference.
pub media: Vec<slidecraft_model::MediaItem>, pub media: Vec<deckcraft_model::MediaItem>,
pub text: Option<slidecraft_model::TextBody>, pub text: Option<deckcraft_model::TextBody>,
pub plain: String, pub plain: String,
/// How many times the shapes were pasted (each paste offsets further). /// How many times the shapes were pasted (each paste offsets further).
pub pastes: u32, pub pastes: u32,

View File

@ -3,6 +3,6 @@
pub const DISCORD: &str = "https://discord.gg/artcraft"; pub const DISCORD: &str = "https://discord.gg/artcraft";
pub const WEBSITE: &str = "https://getartcraft.com/"; pub const WEBSITE: &str = "https://getartcraft.com/";
pub const APPS: &str = "https://getartcraft.com/apps"; pub const APPS: &str = "https://getartcraft.com/apps";
pub const APP_PAGE: &str = "https://getartcraft.com/apps/slidecraft"; pub const APP_PAGE: &str = "https://getartcraft.com/apps/deckcraft";
pub const GITHUB: &str = "https://github.com/storytold/slidecraft"; pub const GITHUB: &str = "https://github.com/storytold/deckcraft";
pub const ISSUES: &str = "https://github.com/storytold/slidecraft/issues"; pub const ISSUES: &str = "https://github.com/storytold/deckcraft/issues";

View File

@ -4,7 +4,7 @@
//! `file.recovery.open` reopens what was left there as unsaved presentations that remember where //! `file.recovery.open` reopens what was left there as unsaved presentations that remember where
//! they were saved. //! they were saved.
//! //!
//! An entry is `<uid>.slidecraft` (the presentation) plus `<uid>.json` (`{"path", "title", //! An entry is `<uid>.deckcraft` (the presentation) plus `<uid>.json` (`{"path", "title",
//! "saved"}`: the original file, the title and when it was written, Unix seconds). //! "saved"}`: the original file, the title and when it was written, Unix seconds).
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -17,16 +17,16 @@ use crate::{DocState, EngineError, Result, Session};
pub fn default_dir() -> Option<PathBuf> { pub fn default_dir() -> Option<PathBuf> {
let home = std::env::var_os("HOME").map(PathBuf::from); let home = std::env::var_os("HOME").map(PathBuf::from);
if cfg!(target_os = "macos") { 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") { 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) { 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 { fn now_secs() -> u64 {
@ -58,7 +58,7 @@ pub fn save(s: &Session, dir: &Path) -> Result<usize> {
continue; continue;
} }
let (doc, meta) = files(dir, d.uid); 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. // Write then rename, so a crash mid-write never leaves a torn file.
let tmp = doc.with_extension("tmp"); 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())))?; 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 }; let Ok(rd) = std::fs::read_dir(dir) else { return out };
for e in rd.flatten() { for e in rd.flatten() {
let p = e.path(); 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; continue;
} }
let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::<u64>().ok()) else { continue }; let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::<u64>().ok()) else { continue };
@ -98,8 +98,8 @@ pub fn open(s: &mut Session, dir: &Path) -> Result<Vec<usize>> {
for (uid, meta) in list(dir) { for (uid, meta) in list(dir) {
let (doc, _) = files(dir, uid); let (doc, _) = files(dir, uid);
let Ok(bytes) = std::fs::read(&doc) else { continue }; let Ok(bytes) = std::fs::read(&doc) else { continue };
let Ok(mut d) = slidecraft_format::load(&bytes) else { continue }; let Ok(mut d) = deckcraft_format::load(&bytes) else { continue };
slidecraft_format::repair(&mut d); deckcraft_format::repair(&mut d);
let path = meta.get("path").and_then(Value::as_str).map(str::to_string); 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 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)")); let mut st = DocState::new(d, path, format!("{title} (Recovered)"));
@ -117,7 +117,7 @@ mod tests {
use super::*; use super::*;
fn temp(name: &str) -> PathBuf { 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); let _ = std::fs::remove_dir_all(&dir);
dir dir
} }
@ -143,7 +143,7 @@ mod tests {
// Saving drops the entry. // Saving drops the entry.
s.recovery_dir = Some(dir.clone()); s.recovery_dir = Some(dir.clone());
assert_eq!(save(&s, &dir).unwrap(), 1); 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(); s.execute("file.saveAs", &json!({"path": path.to_string_lossy()})).unwrap();
assert!(list(&dir).is_empty()); assert!(list(&dir).is_empty());
let _ = std::fs::remove_dir_all(&dir); let _ = std::fs::remove_dir_all(&dir);
@ -153,7 +153,7 @@ mod tests {
fn corrupt_entries_are_skipped() { fn corrupt_entries_are_skipped() {
let dir = temp("recovery-corrupt"); let dir = temp("recovery-corrupt");
std::fs::create_dir_all(&dir).unwrap(); 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(); std::fs::write(dir.join("7.json"), b"[1, 2]").unwrap();
let mut s = Session::new(); let mut s = Session::new();
assert!(open(&mut s, &dir).unwrap().is_empty()); assert!(open(&mut s, &dir).unwrap().is_empty());

View File

@ -1,5 +1,5 @@
//! A sample deck built entirely in code (our own text, shapes and data), for demos, screenshots //! 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}; use serde_json::{Value, json};
@ -15,13 +15,13 @@ fn last_id(v: &Value) -> u64 {
/// Open the sample presentation in a new document. /// Open the sample presentation in a new document.
pub fn open_sample(s: &mut Session) -> Result<()> { 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 st = s.doc()?;
let title = st.current_slide().and_then(|x| x.shapes.first()).map(|x| x.id.0).unwrap_or(0); 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); 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": 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, "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. // Decorative bands on the title slide.
let b = run(s, "shape.insert", json!({"preset": "rect", "rect": [0, 470, 960, 70]}))?; 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}}))?; 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"}))?; run(s, "transition.set", json!({"kind": "split"}))?;
// Closing. // 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]}))?; 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.fill", json!({"id": last_id(&moon), "color": "accent6"}))?;
run(s, "shape.line", json!({"id": last_id(&moon), "none": true}))?; run(s, "shape.line", json!({"id": last_id(&moon), "none": true}))?;

View File

@ -174,7 +174,7 @@ fn sample_deck_builds_and_renders() {
assert!(st.doc.validate().is_empty()); assert!(st.doc.validate().is_empty());
assert!(!st.is_dirty()); assert!(!st.is_dirty());
for i in 0..st.doc.slides.len() { 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); assert_eq!(img.width, 192);
} }
let v = s.execute("document.inspect", &json!({})).unwrap(); let v = s.execute("document.inspect", &json!({})).unwrap();
@ -187,7 +187,7 @@ fn save_and_reopen_native() {
sample::open_sample(&mut s).unwrap(); sample::open_sample(&mut s).unwrap();
let bytes = s.execute("file.saveBytes", &json!({})).unwrap()["data"].as_str().unwrap().to_string(); let bytes = s.execute("file.saveBytes", &json!({})).unwrap()["data"].as_str().unwrap().to_string();
let n = s.doc().unwrap().doc.slides.len(); 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.doc().unwrap().doc.slides.len(), n);
assert_eq!(s.documents().len(), 2); 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")); 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 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)) 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(); s.execute("shape.move", &json!({"dx": 50, "dy": 20})).unwrap();
let (q0, q1) = line_ends(&s, c); let (q0, q1) = line_ends(&s, c);
assert!((q0 - p0).hypot() < 1e-6); 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. // Undo puts both back in one step.
s.execute("edit.undo", &json!({})).unwrap(); s.execute("edit.undo", &json!({})).unwrap();
assert!((line_ends(&s, c).1 - p1).hypot() < 1e-6); 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.select", &json!({"ids": [a]})).unwrap();
s.execute("edit.delete", &json!({})).unwrap(); s.execute("edit.delete", &json!({})).unwrap();
let st = s.doc().unwrap(); let st = s.doc().unwrap();
let sh = st.shape(slidecraft_model::ShapeId(c as u32)).unwrap(); let sh = st.shape(deckcraft_model::ShapeId(c as u32)).unwrap();
assert!(matches!(sh.kind, slidecraft_model::ShapeKind::Connector { start: None, end: Some(_) })); assert!(matches!(sh.kind, deckcraft_model::ShapeKind::Connector { start: None, end: Some(_) }));
} }
#[test] #[test]
@ -301,7 +301,7 @@ fn drawing_a_line_between_shapes_glues_it() {
let st = s.doc().unwrap(); let st = s.doc().unwrap();
let line = st.shapes().iter().find(|x| x.is_line()).unwrap(); let line = st.shapes().iter().find(|x| x.is_line()).unwrap();
match line.kind { 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)); assert_eq!((x.0 as u64, y.0 as u64), (a, b));
} }
ref k => panic!("not glued: {k:?}"), ref k => panic!("not glued: {k:?}"),
@ -336,7 +336,7 @@ fn freeform_tools_and_command() {
let st = s.doc().unwrap(); let st = s.doc().unwrap();
let sh = st.shapes().last().unwrap(); let sh = st.shapes().last().unwrap();
match &sh.geom { 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:?}"), g => panic!("{g:?}"),
} }
let x = sh.xfrm.unwrap(); let x = sh.xfrm.unwrap();
@ -359,10 +359,10 @@ fn freeform_tools_and_command() {
s.pointer(ev(tools::PointerKind::Up, x, y)).unwrap(); s.pointer(ev(tools::PointerKind::Up, x, y)).unwrap();
} }
let r = s.key("Enter", Default::default()).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(); let st = s.doc().unwrap();
match &st.shape(id).unwrap().geom { 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:?}"), g => panic!("{g:?}"),
} }
} }

View File

@ -3,10 +3,10 @@
use std::sync::Arc; use std::sync::Arc;
use deckcraft_geom::{Affine, Point, Rect, Vec2, Xfrm};
use deckcraft_model::{Shape, ShapeId, ShapeKind};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Value, json}; 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::cmd::{self, xfrm_of};
use crate::{HistoryEntry, Result, Session, TextSel}; use crate::{HistoryEntry, Result, Session, TextSel};
@ -55,7 +55,7 @@ pub enum ToolKind {
/// Ink: pen (`pen`, `highlighter`) or `eraser`. /// Ink: pen (`pen`, `highlighter`) or `eraser`.
Ink { Ink {
mode: String, mode: String,
color: slidecraft_color::Rgba, color: deckcraft_color::Rgba,
width: f64, width: f64,
}, },
} }
@ -229,7 +229,7 @@ impl Session {
} }
continue; 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 { for hd in &geo.handles {
if (x.affine() * hd.pos - p).hypot() <= tol * 1.6 { if (x.affine() * hd.pos - p).hypot() <= tol * 1.6 {
return Some(Hit::Adjust { id: *id, index: hd.index }); return Some(Hit::Adjust { id: *id, index: hd.index });
@ -277,7 +277,7 @@ impl Session {
return Ok(Value::Null); return Ok(Value::Null);
} }
self.tool.hover = self.hit_test(p, tol); 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 { if line_tool {
self.update_sites(p, tol, None); self.update_sites(p, tol, None);
} else { } else {
@ -485,7 +485,7 @@ impl Session {
self.edit(|doc, sel| { self.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?; let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?;
for (id, o) in &origs2 { 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 }); 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 st = self.doc()?;
let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) }; let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) };
let local = orig.affine().inverse() * p; 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 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 free2d = matches!(sh.geom.preset_name(), Some(n) if n.contains("Callout") && !n.contains("Arrow"));
let vals: Vec<(usize, f64)> = if free2d { let vals: Vec<(usize, f64)> = if free2d {
@ -568,10 +568,10 @@ impl Session {
}; };
self.edit(|doc, sel| { self.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?; 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) if let Some(sh) = deckcraft_model::find_shape_mut(list, id)
&& let slidecraft_model::Geom::Preset { name, adj } = &mut sh.geom && 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 { for (i, v) in &vals {
while adj.len() <= *i { while adj.len() <= *i {
let k = adj.len(); let k = adj.len();
@ -589,7 +589,7 @@ impl Session {
self.tool.marquee = Some(Rect::from_points(start, p)); self.tool.marquee = Some(Rect::from_points(start, p));
} }
Drag::Create { start } => { 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 x = if line {
let mut q = if mods.shift { constrain_45(start, p) } else { p }; let mut q = if mods.shift { constrain_45(start, p) } else { p };
if let Some((_, _, at)) = self.update_sites(q, tol, None) { if let Some((_, _, at)) = self.update_sites(q, tol, None) {
@ -693,7 +693,7 @@ impl Session {
let dragged = (p - start).hypot() > 2.0; let dragged = (p - start).hypot() > 2.0;
let res = match &kind { let res = match &kind {
ToolKind::Shape { preset } => { 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) { let rect = match preview.filter(|_| dragged) {
Some(x) => x, Some(x) => x,
None if line => Xfrm::new(start.x, start.y, 72.0, 0.0), 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 x = xfrm_of(&st.doc, &st.selection, &sh);
let local = x.affine().inverse() * p; let local = x.affine().inverse() * p;
if let ShapeKind::Table(t) = &sh.kind { 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]})); return self.execute("text.edit", &json!({"id": id, "cell": [cell.0, cell.1]}));
} }
Ok(Value::Null) Ok(Value::Null)
@ -857,7 +857,7 @@ impl Session {
fn set_xfrm(&mut self, id: ShapeId, x: Xfrm) -> Result<()> { fn set_xfrm(&mut self, id: ShapeId, x: Xfrm) -> Result<()> {
self.edit(|doc, sel| { self.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("tool", "no slide"))?; 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); sh.xfrm = Some(x);
if let ShapeKind::Table(t) = &mut sh.kind { if let ShapeKind::Table(t) = &mut sh.kind {
// Tables resize their columns and rows proportionally. // 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}))?; 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 { if x.flip_h || x.flip_v {
self.edit(|doc, sel| { 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() && let Some(xx) = sh.xfrm.as_mut()
{ {
xx.flip_h = x.flip_h; xx.flip_h = x.flip_h;
@ -969,14 +969,14 @@ impl Session {
let rr = r + Vec2::new(d, 0.0); 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) }); 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 { } 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 { if let Some((d, y, b)) = best_y {
off.y = d; off.y = d;
let rr = r + Vec2::new(0.0, 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) }); 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 { } 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) (off, guides)
} }
@ -1014,7 +1014,7 @@ impl Session {
fn add_ink(&mut self, points: Vec<(f64, f64, f32)>) -> Result<Value> { fn add_ink(&mut self, points: Vec<(f64, f64, f32)>) -> Result<Value> {
let ToolKind::Ink { mode, color, width } = self.tool.kind.clone() else { return Ok(Value::Null) }; 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). // 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); let existing = self.doc()?.shapes().iter().rev().find(|s| matches!(s.kind, ShapeKind::Ink { .. })).map(|s| s.id);
self.edit(|doc, sel| { 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"))?; 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 && let ShapeKind::Ink { strokes } = &mut sh.kind
{ {
strokes.push(stroke); strokes.push(stroke);
@ -1051,7 +1051,7 @@ impl Session {
let sel_before = st.selection.clone(); let sel_before = st.selection.clone();
self.edit(|doc, sel| { self.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("ink", "no slide"))?; 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 && 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)); 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<Hit
ShapeKind::Group { children, child } => { ShapeKind::Group { children, child } => {
// A group is hit when any child is hit; the group is what gets selected. // 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 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; let q = m.inverse() * p;
for c in children.iter().rev() { for c in children.iter().rev() {
if let Some(cx) = c.xfrm { if let Some(cx) = c.xfrm {
@ -1208,9 +1208,9 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
} }
_ => { _ => {
if sh.is_line() { 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); 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 Some(Hit::Shape { id: sh.id, text: false });
} }
return None; return None;
@ -1221,11 +1221,11 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
} }
let has_text = matches!(sh.kind, ShapeKind::Shape) && (sh.text.as_ref().is_some_and(|t| !t.is_empty()) || sh.ph.is_some() || sh.text_box); let has_text = matches!(sh.kind, ShapeKind::Shape) && (sh.text.as_ref().is_some_and(|t| !t.is_empty()) || sh.ph.is_some() || sh.text_box);
// Empty interiors of unfilled, text-less shapes don't catch clicks. // Empty interiors of unfilled, text-less shapes don't catch clicks.
let filled = !matches!(sh.fill, Some(slidecraft_model::Fill::None)) || sh.style.is_some(); let filled = !matches!(sh.fill, Some(deckcraft_model::Fill::None)) || sh.style.is_some();
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h); let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
let in_geo = { let in_geo = {
use slidecraft_geom::KurboShape; use deckcraft_geom::KurboShape;
geo.outline().winding(local) != 0 || slidecraft_geom::near_path(&geo.outline(), local, tol) geo.outline().winding(local) != 0 || deckcraft_geom::near_path(&geo.outline(), local, tol)
}; };
if has_text if has_text
|| sh.ph.is_some() || sh.ph.is_some()
@ -1234,7 +1234,7 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
ShapeKind::Picture { .. } | ShapeKind::Table(_) | ShapeKind::Chart(_) | ShapeKind::Media(_) | ShapeKind::Opaque { .. } ShapeKind::Picture { .. } | ShapeKind::Table(_) | ShapeKind::Chart(_) | ShapeKind::Media(_) | ShapeKind::Opaque { .. }
) )
|| (filled && in_geo) || (filled && in_geo)
|| slidecraft_geom::near_path(&geo.outline(), local, tol) || deckcraft_geom::near_path(&geo.outline(), local, tol)
{ {
return Some(Hit::Shape { id: sh.id, text: has_text || matches!(sh.kind, ShapeKind::Shape) }); return Some(Hit::Shape { id: sh.id, text: has_text || matches!(sh.kind, ShapeKind::Shape) });
} }
@ -1258,7 +1258,7 @@ pub fn text_pos(st: &crate::DocState, sh: &Shape, p: Point) -> (usize, usize) {
} }
/// Which table cell contains a table-local point. /// 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 y = 0.0;
let mut row = t.rows.len().saturating_sub(1); let mut row = t.rows.len().saturating_sub(1);
for (i, r) in t.rows.iter().enumerate() { for (i, r) in t.rows.iter().enumerate() {

View File

@ -1,9 +1,9 @@
[package] [package]
name = "slidecraft-fonts" name = "deckcraft-fonts"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.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] [lints]
workspace = true workspace = true

View File

@ -7,7 +7,7 @@
use std::fmt::Write as _; use std::fmt::Write as _;
use std::path::PathBuf; 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"]; const EXCLUDED_FAMILIES: &[&str] = &["Source Sans 3", "Source Serif 4", "Source Han Sans", "Source Han Serif", "Noto Sans CJK SC"];
fn main() { fn main() {
@ -53,7 +53,7 @@ fn manifest(dir: &std::path::Path, manifest: &std::path::Path) -> Result<String,
let [family, style, file, scripts, ..] = f.as_slice() else { let [family, style, file, scripts, ..] = f.as_slice() else {
return Err(format!("malformed manifest line: {line}")); return Err(format!("malformed manifest line: {line}"));
}; };
// SlideCraft's asset policy: no Adobe-authored typefaces, even openly licensed ones. // DeckCraft's asset policy: no Adobe-authored typefaces, even openly licensed ones.
if EXCLUDED_FAMILIES.contains(family) { if EXCLUDED_FAMILIES.contains(family) {
continue; continue;
} }

View File

@ -1,9 +1,9 @@
//! SlideCraft fonts: the font database (craft-fonts families, user and system fonts, and open //! DeckCraft fonts: the font database (craft-fonts families, user and system fonts, and open
//! substitutes for the Office fonts presentations ask for), vertical metrics, glyph outlines and //! substitutes for the Office fonts presentations ask for), vertical metrics, glyph outlines and
//! OpenType shaping. //! OpenType shaping.
//! //!
//! Shaping here is style-agnostic: [`shape`] turns a string in one face into glyph ids, clusters //! Shaping here is style-agnostic: [`shape`] turns a string in one face into glyph ids, clusters
//! and advances in font units. `slidecraft-text` applies sizes, tracking, scaling and //! and advances in font units. `deckcraft-text` applies sizes, tracking, scaling and
//! justification on top. //! justification on top.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]

View File

@ -1,15 +1,15 @@
[package] [package]
name = "slidecraft-format" name = "deckcraft-format"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
description = "SlideCraft native file format (.slidecraft: zip of a JSON manifest plus media) and outline text import/export" description = "DeckCraft native file format (.deckcraft: zip of a JSON manifest plus media) and outline text import/export"
[lints] [lints]
workspace = true workspace = true
[dependencies] [dependencies]
slidecraft-model = { workspace = true } deckcraft-model = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
zip = { workspace = true } zip = { workspace = true }

View File

@ -1,4 +1,4 @@
//! The native `.slidecraft` format: a zip holding `presentation.json` (the model, minus media //! The native `.deckcraft` format: a zip holding `presentation.json` (the model, minus media
//! bytes), `media/<id>` files with the embedded media, and `mimetype`. Readers ignore unknown //! bytes), `media/<id>` files with the embedded media, and `mimetype`. Readers ignore unknown
//! fields, so newer files open in older builds with what those builds understand. //! 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)] #![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::io::{Cursor, Read, Write};
use std::sync::Arc; use std::sync::Arc;
use slidecraft_model::text::{Paragraph, TextBody}; use deckcraft_model::text::{Paragraph, TextBody};
use slidecraft_model::{LayoutType, Presentation, defaults}; use deckcraft_model::{LayoutType, Presentation, defaults};
pub const MIMETYPE: &str = "application/vnd.storyteller.slidecraft+zip"; pub const MIMETYPE: &str = "application/vnd.storyteller.deckcraft+zip";
pub const EXTENSION: &str = "slidecraft"; /// 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. /// Format version written into the manifest.
pub const VERSION: u32 = 1; pub const VERSION: u32 = 1;
/// Largest entry we will inflate (guards against zip bombs). /// Largest entry we will inflate (guards against zip bombs).
@ -19,7 +21,7 @@ const MAX_ENTRY: u64 = 2 << 30;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum FormatError { pub enum FormatError {
#[error("not a SlideCraft file: {0}")] #[error("not a DeckCraft file: {0}")]
NotOurs(String), NotOurs(String),
#[error("damaged file: {0}")] #[error("damaged file: {0}")]
Damaged(String), Damaged(String),
@ -48,9 +50,9 @@ pub fn save(p: &Presentation) -> Result<Vec<u8>> {
z.start_file("mimetype", stored).map_err(w)?; z.start_file("mimetype", stored).map_err(w)?;
z.write_all(MIMETYPE.as_bytes()).map_err(|e| FormatError::Write(e.to_string()))?; z.write_all(MIMETYPE.as_bytes()).map_err(|e| FormatError::Write(e.to_string()))?;
let m = Manifest { let m = Manifest {
format: "slidecraft".into(), format: "deckcraft".into(),
version: VERSION, version: VERSION,
generator: format!("SlideCraft {}", env!("CARGO_PKG_VERSION")), generator: format!("DeckCraft {}", env!("CARGO_PKG_VERSION")),
presentation: p.clone(), presentation: p.clone(),
}; };
let json = serde_json::to_vec(&m).map_err(|e| FormatError::Write(e.to_string()))?; 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") || ct.contains("zip")
} }
/// Does this look like a `.slidecraft` file? /// Does this look like a `.deckcraft` file?
pub fn sniff(bytes: &[u8]) -> bool { 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<Cursor<&[u8]>>, name: &str) -> Result<Vec<u8>> { fn read_entry(z: &mut zip::ZipArchive<Cursor<&[u8]>>, name: &str) -> Result<Vec<u8>> {
@ -99,7 +101,7 @@ pub fn load(bytes: &[u8]) -> Result<Presentation> {
let mut z = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| FormatError::NotOurs(e.to_string()))?; 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 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()))?; 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)); return Err(FormatError::NotOurs(m.format));
} }
let mut p = m.presentation; 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 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)); let dup = p.slides.get(i).is_some_and(|s| !seen.insert(s.id));
if needs_layout || dup { 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) { if let Some(s) = p.slides.get_mut(i) {
let s = Arc::make_mut(s); let s = Arc::make_mut(s);
if needs_layout { if needs_layout {
@ -150,8 +152,8 @@ pub fn repair(p: &mut Presentation) {
pub fn outline_to_slides(p: &mut Presentation, text: &str) -> usize { 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 Some(lay) = defaults::layout_of_kind(p, LayoutType::TitleAndContent) else { return 0 };
let mut made = 0; let mut made = 0;
let mut cur: Option<slidecraft_model::Slide> = None; let mut cur: Option<deckcraft_model::Slide> = None;
let flush = |p: &mut Presentation, s: Option<slidecraft_model::Slide>, made: &mut usize| { let flush = |p: &mut Presentation, s: Option<deckcraft_model::Slide>, made: &mut usize| {
if let Some(s) = s { if let Some(s) = s {
p.slides.push(Arc::new(s)); p.slides.push(Arc::new(s));
*made += 1; *made += 1;
@ -242,7 +244,7 @@ mod tests {
#[test] #[test]
fn repair_fixes_missing_layout_and_size() { fn repair_fixes_missing_layout_and_size() {
let mut p = Presentation::default(); 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; p.slide_size.width = f64::NAN;
repair(&mut p); repair(&mut p);
assert!(p.validate().is_empty()); assert!(p.validate().is_empty());

View File

@ -1,9 +1,9 @@
[package] [package]
name = "slidecraft-geom" name = "deckcraft-geom"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.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] [lints]
workspace = true workspace = true

View File

@ -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 //! 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 //! (English Metric Units, 12 700 per point); [`emu_to_pt`] and [`pt_to_emu`] convert losslessly for

View File

@ -1,5 +1,5 @@
[package] [package]
name = "slidecraft-h264" name = "deckcraft-h264"
description = "Clean-room pure-Rust H.264/AVC decoder (ITU-T H.264)" description = "Clean-room pure-Rust H.264/AVC decoder (ITU-T H.264)"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
@ -14,6 +14,6 @@ default = ["threads"]
threads = ["dep:rayon"] threads = ["dep:rayon"]
[dependencies] [dependencies]
slidecraft-bitstream = { workspace = true } deckcraft-bitstream = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
rayon = { workspace = true, optional = true } rayon = { workspace = true, optional = true }

View File

@ -2,7 +2,7 @@
use crate::cavlc_tables::*; use crate::cavlc_tables::*;
use crate::error::{Result, ensure}; use crate::error::{Result, ensure};
use slidecraft_bitstream::BitReader; use deckcraft_bitstream::BitReader;
use std::sync::OnceLock; use std::sync::OnceLock;
/// Prefix-code lookup keyed by (number of leading zeros, next 5 bits after the first one bit). /// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use slidecraft_bitstream::BitWriter; use deckcraft_bitstream::BitWriter;
fn kraft(codes: &[&str]) -> f64 { fn kraft(codes: &[&str]) -> f64 {
codes.iter().filter(|c| !c.is_empty()).map(|c| 0.5f64.powi(c.len() as i32)).sum() codes.iter().filter(|c| !c.is_empty()).map(|c| 0.5f64.powi(c.len() as i32)).sum()

View File

@ -480,7 +480,7 @@ mod tests {
mod speed { mod speed {
use super::*; 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] #[test]
#[ignore] #[ignore]
fn edge_filter_speed() { fn edge_filter_speed() {

View File

@ -14,7 +14,7 @@ use crate::slice::{NalHeader, Poc, PocState, SliceHeader, SliceType, nal_type};
use crate::slicedec::{PicState, SliceDecoder}; use crate::slicedec::{PicState, SliceDecoder};
use crate::transform::LevelScale; use crate::transform::LevelScale;
use crate::{ColorInfo, Picture}; 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::collections::VecDeque;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};

View File

@ -1,4 +1,4 @@
use slidecraft_bitstream::BitError; use deckcraft_bitstream::BitError;
/// Errors produced by the decoder. /// Errors produced by the decoder.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]

View File

@ -5,7 +5,7 @@
//! multiple slices, long-term references and all MMCOs, deblocking. //! multiple slices, long-term references and all MMCOs, deblocking.
//! //!
//! ```no_run //! ```no_run
//! let mut dec = slidecraft_h264::Decoder::new(); //! let mut dec = deckcraft_h264::Decoder::new();
//! let stream = std::fs::read("video.h264").unwrap(); //! let stream = std::fs::read("video.h264").unwrap();
//! for pic in dec.decode(&stream, 0).unwrap() { //! for pic in dec.decode(&stream, 0).unwrap() {
//! println!("{}x{} poc {}", pic.width, pic.height, pic.poc); //! println!("{}x{} poc {}", pic.width, pic.height, pic.poc);

View File

@ -2,7 +2,7 @@
use crate::error::{Result, ensure, unsupported}; use crate::error::{Result, ensure, unsupported};
use crate::tables::*; use crate::tables::*;
use slidecraft_bitstream::BitReader; use deckcraft_bitstream::BitReader;
/// Scaling matrices after applying fall-back rules, stored in raster order (weightScale). /// Scaling matrices after applying fall-back rules, stored in raster order (weightScale).
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -665,7 +665,7 @@ impl Pps {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use slidecraft_bitstream::BitWriter; use deckcraft_bitstream::BitWriter;
#[test] #[test]
fn scaling_list_default_signal() { fn scaling_list_default_signal() {

View File

@ -2,7 +2,7 @@
use crate::error::{Result, ensure}; use crate::error::{Result, ensure};
use crate::params::{Pps, Sps}; use crate::params::{Pps, Sps};
use slidecraft_bitstream::BitReader; use deckcraft_bitstream::BitReader;
/// NAL unit types used by the decoder. /// NAL unit types used by the decoder.
pub mod nal_type { pub mod nal_type {

View File

@ -11,7 +11,7 @@ use crate::picture::*;
use crate::slice::{SliceHeader, SliceType}; use crate::slice::{SliceHeader, SliceType};
use crate::tables::*; use crate::tables::*;
use crate::transform::{self, LevelScale}; use crate::transform::{self, LevelScale};
use slidecraft_bitstream::BitReader; use deckcraft_bitstream::BitReader;
/// Per-slice data kept with the picture (deblocking, motion field export). /// Per-slice data kept with the picture (deblocking, motion field export).
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]

View File

@ -6,7 +6,7 @@
use crate::Decoder; use crate::Decoder;
use crate::cabac::{Cabac, NEXT_STATE, RANGE_TAB_LPS}; 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_W: usize = 4;
const MB_H: usize = 3; const MB_H: usize = 3;

View File

@ -1,5 +1,5 @@
[package] [package]
name = "slidecraft-hevc" name = "deckcraft-hevc"
description = "Clean-room pure-Rust H.265/HEVC decoder (ITU-T H.265)" description = "Clean-room pure-Rust H.265/HEVC decoder (ITU-T H.265)"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
@ -14,6 +14,6 @@ default = ["threads"]
threads = ["dep:rayon"] threads = ["dep:rayon"]
[dependencies] [dependencies]
slidecraft-bitstream = { workspace = true } deckcraft-bitstream = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
rayon = { workspace = true, optional = true } rayon = { workspace = true, optional = true }

View File

@ -12,7 +12,7 @@ use crate::picture::{Frame, FrameRef};
use crate::slice::{NalHeader, SliceHeader, nal_type}; use crate::slice::{NalHeader, SliceHeader, nal_type};
use crate::slicedec::{F_INTRA, F_SKIP, PicState, SliceDecoder, SliceJob}; use crate::slicedec::{F_INTRA, F_SKIP, PicState, SliceDecoder, SliceJob};
use crate::{ColorInfo, Picture, Plane}; 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::collections::VecDeque;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};

View File

@ -1,4 +1,4 @@
use slidecraft_bitstream::BitError; use deckcraft_bitstream::BitError;
/// Errors produced by the decoder. /// Errors produced by the decoder.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]

View File

@ -7,7 +7,7 @@
//! (parsing), dependent slice segments, short- and long-term reference picture sets. //! (parsing), dependent slice segments, short- and long-term reference picture sets.
//! //!
//! ```no_run //! ```no_run
//! let mut dec = slidecraft_hevc::Decoder::new(); //! let mut dec = deckcraft_hevc::Decoder::new();
//! let stream = std::fs::read("video.h265").unwrap(); //! let stream = std::fs::read("video.h265").unwrap();
//! for pic in dec.decode(&stream, 0).unwrap() { //! for pic in dec.decode(&stream, 0).unwrap() {
//! println!("{}x{} poc {}", pic.width, pic.height, pic.poc); //! println!("{}x{} poc {}", pic.width, pic.height, pic.poc);

View File

@ -4,7 +4,7 @@
use crate::error::{Result, ensure, unsupported}; use crate::error::{Result, ensure, unsupported};
use crate::spec_tables::{DEFAULT_SCALING_INTER, DEFAULT_SCALING_INTRA}; use crate::spec_tables::{DEFAULT_SCALING_INTER, DEFAULT_SCALING_INTRA};
use crate::tables::scan_diag; use crate::tables::scan_diag;
use slidecraft_bitstream::BitReader; use deckcraft_bitstream::BitReader;
/// General profile / tier / level information. /// General profile / tier / level information.
#[derive(Clone, Debug, Default, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]

View File

@ -2,7 +2,7 @@
use crate::error::{Result, ensure, invalid}; use crate::error::{Result, ensure, invalid};
use crate::params::{Pps, Sps, StRps}; use crate::params::{Pps, Sps, StRps};
use slidecraft_bitstream::BitReader; use deckcraft_bitstream::BitReader;
pub mod nal_type { pub mod nal_type {
pub const TRAIL_N: u8 = 0; pub const TRAIL_N: u8 = 0;

Some files were not shown because too many files have changed in this diff Show More