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>
|
|
@ -46,12 +46,12 @@ jobs:
|
|||
}
|
||||
- name: XML, desktop entry, AppStream, Flatpak manifest
|
||||
run: |
|
||||
xmllint --noout packaging/windows/slidecraft.wxs packaging/macos/*.plist* packaging/linux/*.xml*
|
||||
xmllint --noout packaging/windows/deckcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml*
|
||||
# Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon.
|
||||
if grep -oE '<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
|
||||
fi
|
||||
desktop-file-validate packaging/linux/ai.storyteller.slidecraft.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"
|
||||
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.slidecraft.metainfo.xml"
|
||||
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.slidecraft.yml')); assert m['id'] == 'ai.storyteller.slidecraft' and m['command'] == 'slidecraft'"
|
||||
desktop-file-validate packaging/linux/ai.storyteller.deckcraft.desktop
|
||||
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.deckcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.deckcraft.metainfo.xml"
|
||||
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.deckcraft.metainfo.xml"
|
||||
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.deckcraft.yml')); assert m['id'] == 'ai.storyteller.deckcraft' and m['command'] == 'deckcraft'"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Release pipeline: every push to `release` builds installers for every platform and creates or
|
||||
# updates the draft GitHub Release "SlideCraft v<version>" (tag v<version>). Recipe:
|
||||
# updates the draft GitHub Release "DeckCraft v<version>" (tag v<version>). Recipe:
|
||||
# craftrules release/playbook.md.
|
||||
#
|
||||
# The version has one source: `[workspace.package] version` in Cargo.toml (`cargo xtask version`,
|
||||
|
|
@ -33,7 +33,7 @@ env:
|
|||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
SLIDECRAFT_BUILD_SHA: ${{ github.sha }}
|
||||
DECKCRAFT_BUILD_SHA: ${{ github.sha }}
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
# Optional font input (storytold/craft-fonts docs/integration.md); releases always embed it.
|
||||
# This commit adds fonts/latin-manifest.txt (the Latin presentation fonts).
|
||||
|
|
@ -66,7 +66,7 @@ jobs:
|
|||
echo "date=$(date -u +%Y-%m-%d)"
|
||||
echo "prerelease=$pre"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
echo "### SlideCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
||||
echo "### DeckCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
||||
|
||||
macos:
|
||||
name: macOS universal
|
||||
|
|
@ -74,8 +74,8 @@ jobs:
|
|||
runs-on: macos-15
|
||||
environment: release
|
||||
env:
|
||||
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check out craft-fonts
|
||||
|
|
@ -92,7 +92,7 @@ jobs:
|
|||
key: release-macos-universal
|
||||
- name: Apply version override
|
||||
if: inputs.version != ''
|
||||
run: cargo xtask version set "$SLIDECRAFT_VERSION"
|
||||
run: cargo xtask version set "$DECKCRAFT_VERSION"
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
|
|
@ -129,8 +129,8 @@ jobs:
|
|||
- { arch: x64, target: x86_64-pc-windows-msvc }
|
||||
- { arch: x86, target: i686-pc-windows-msvc }
|
||||
env:
|
||||
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check out craft-fonts
|
||||
|
|
@ -148,7 +148,7 @@ jobs:
|
|||
- name: Apply version override
|
||||
if: inputs.version != ''
|
||||
shell: bash
|
||||
run: cargo xtask version set "$SLIDECRAFT_VERSION"
|
||||
run: cargo xtask version set "$DECKCRAFT_VERSION"
|
||||
- name: Install WiX
|
||||
shell: pwsh
|
||||
run: dotnet tool install --global wix --version 5.0.2
|
||||
|
|
@ -186,8 +186,8 @@ jobs:
|
|||
- { arch: x86_64, runner: ubuntu-22.04, deb: amd64 }
|
||||
- { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 }
|
||||
env:
|
||||
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
NFPM_VERSION: 2.47.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -212,7 +212,7 @@ jobs:
|
|||
key: release-linux-${{ matrix.arch }}
|
||||
- name: Apply version override
|
||||
if: inputs.version != ''
|
||||
run: cargo xtask version set "$SLIDECRAFT_VERSION"
|
||||
run: cargo xtask version set "$DECKCRAFT_VERSION"
|
||||
- name: Build and package (AppImage, deb, rpm, tar.gz)
|
||||
env:
|
||||
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
|
||||
|
|
@ -221,8 +221,8 @@ jobs:
|
|||
- name: Check the packages
|
||||
run: |
|
||||
dpkg-deb --info dist/release/*.deb
|
||||
dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/slidecraft$|metainfo|applications'
|
||||
ldd target/release/slidecraft
|
||||
dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/deckcraft$|metainfo|applications'
|
||||
ldd target/release/deckcraft
|
||||
for appimage in dist/release/*.AppImage; do
|
||||
chmod +x "$appimage"
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version
|
||||
|
|
@ -244,14 +244,14 @@ jobs:
|
|||
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
|
||||
options: --privileged
|
||||
env:
|
||||
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Apply version override
|
||||
if: inputs.version != ''
|
||||
run: |
|
||||
# No cargo in this container: the same edit `cargo xtask version set` makes.
|
||||
sed -i "0,/^version = \".*\"/s//version = \"$SLIDECRAFT_VERSION\"/" Cargo.toml
|
||||
sed -i "0,/^version = \".*\"/s//version = \"$DECKCRAFT_VERSION\"/" Cargo.toml
|
||||
grep -m1 '^version' Cargo.toml
|
||||
- name: Vendor the crate sources (flatpak builds run offline)
|
||||
run: |
|
||||
|
|
@ -262,14 +262,14 @@ jobs:
|
|||
python3 "$RUNNER_TEMP/flatpak-cargo-generator.py" Cargo.lock -o packaging/linux/flatpak/cargo-sources.json
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
|
||||
with:
|
||||
manifest-path: packaging/linux/flatpak/ai.storyteller.slidecraft.yml
|
||||
bundle: slidecraft-${{ needs.version.outputs.version }}-linux-x86_64.flatpak
|
||||
manifest-path: packaging/linux/flatpak/ai.storyteller.deckcraft.yml
|
||||
bundle: deckcraft-${{ needs.version.outputs.version }}-linux-x86_64.flatpak
|
||||
upload-artifact: false
|
||||
cache-key: flatpak-builder-${{ github.sha }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-flatpak
|
||||
path: slidecraft-*.flatpak
|
||||
path: deckcraft-*.flatpak
|
||||
if-no-files-found: error
|
||||
|
||||
freebsd:
|
||||
|
|
@ -279,8 +279,8 @@ jobs:
|
|||
environment: release
|
||||
timeout-minutes: 240
|
||||
env:
|
||||
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
VERSION_OVERRIDE: ${{ inputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -297,7 +297,7 @@ jobs:
|
|||
usesh: true
|
||||
cpu: 4
|
||||
mem: 12288
|
||||
envs: SLIDECRAFT_VERSION SLIDECRAFT_BUILD_DATE SLIDECRAFT_BUILD_SHA VERSION_OVERRIDE
|
||||
envs: DECKCRAFT_VERSION DECKCRAFT_BUILD_DATE DECKCRAFT_BUILD_SHA VERSION_OVERRIDE
|
||||
# Copies the workspace (with dist/release) back; the target dir lives outside it so the
|
||||
# copy stays small.
|
||||
copyback: true
|
||||
|
|
@ -308,7 +308,7 @@ jobs:
|
|||
. "$HOME/.cargo/env"
|
||||
git config --global --add safe.directory "*"
|
||||
export CARGO_TARGET_DIR="$HOME/target" CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1
|
||||
if [ -n "$VERSION_OVERRIDE" ]; then cargo xtask version set "$SLIDECRAFT_VERSION"; fi
|
||||
if [ -n "$VERSION_OVERRIDE" ]; then cargo xtask version set "$DECKCRAFT_VERSION"; fi
|
||||
bash packaging/freebsd/package.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
|
@ -322,8 +322,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
env:
|
||||
SLIDECRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SLIDECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
DECKCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
DECKCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
TRUNK_VERSION: 0.21.14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -346,7 +346,7 @@ jobs:
|
|||
trunk --version
|
||||
- name: Apply version override
|
||||
if: inputs.version != ''
|
||||
run: cargo xtask version set "$SLIDECRAFT_VERSION"
|
||||
run: cargo xtask version set "$DECKCRAFT_VERSION"
|
||||
- name: Build and zip
|
||||
env:
|
||||
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
|
||||
|
|
@ -389,7 +389,7 @@ jobs:
|
|||
working-directory: artifacts
|
||||
run: |
|
||||
tag="v$VERSION"
|
||||
title="SlideCraft v$VERSION"
|
||||
title="DeckCraft v$VERSION"
|
||||
pre=()
|
||||
if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi
|
||||
if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then
|
||||
|
|
|
|||
22
AGENTS.md
|
|
@ -1,6 +1,6 @@
|
|||
# AGENTS.md — rules for every agent and contributor
|
||||
|
||||
SlideCraft is a clean-room, open-source, pure-Rust presentation application targeting Microsoft
|
||||
DeckCraft is a clean-room, open-source, pure-Rust presentation application targeting Microsoft
|
||||
PowerPoint parity — and superiority (speed, openness, agent control). It runs natively on macOS,
|
||||
Windows, Linux and FreeBSD, and on the web via WASM. Siblings with the same conventions:
|
||||
PhotoCraft, VectorCraft, FilmCraft, LightCraft, PrintCraft, EffectCraft, DesignCraft
|
||||
|
|
@ -32,7 +32,7 @@ the app renders from those.
|
|||
product" includes PowerPoint, Office, Windows and the fonts they install (Aptos, Calibri,
|
||||
Cambria, Segoe, Consolas, Wingdings…).
|
||||
- This holds even when the vendor publishes the material under an open licence, if it is **visual
|
||||
design**: SlideCraft does not embed or reference Adobe's Source Sans/Serif/Han families or Noto
|
||||
design**: DeckCraft does not embed or reference Adobe's Source Sans/Serif/Han families or Noto
|
||||
CJK (Source Han rebranded); `crates/fonts/build.rs` excludes them.
|
||||
- **Never** recreate, trace, redraw or closely imitate a vendor icon, theme or graphic. Our icons
|
||||
are drawn in code or come from openly licensed sets (Lucide, ISC), and our themes, colours,
|
||||
|
|
@ -58,7 +58,7 @@ Forbidden: system fonts that are not openly licensed, vendor emoji artwork, stoc
|
|||
for personal use" material, screenshots of other people's software, anything whose licence you
|
||||
cannot prove from its original source.
|
||||
|
||||
**Screenshots of SlideCraft** are contributor-original. Everything visible in them must itself be
|
||||
**Screenshots of DeckCraft** are contributor-original. Everything visible in them must itself be
|
||||
allowed: our UI, our/Lucide icons, OFL fonts, and decks built only from allowed assets.
|
||||
|
||||
### 1.3 Every asset is attributed
|
||||
|
|
@ -69,7 +69,7 @@ allowed: our UI, our/Lucide icons, OFL fonts, and decks built only from allowed
|
|||
or `docs/brand/` has no row.
|
||||
- **`NOTICE`** keeps the notices the licences require.
|
||||
- Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this
|
||||
repo (craftrules `standards/fonts.md`). SlideCraft embeds its `fonts/latin-manifest.txt` and
|
||||
repo (craftrules `standards/fonts.md`). DeckCraft embeds its `fonts/latin-manifest.txt` and
|
||||
`fonts/manifest.txt` when built with `CRAFT_FONTS_DIR` (local dev: `.cargo/config.toml` points at
|
||||
`../craft-fonts`).
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ once and tell the user.
|
|||
|
||||
## 3. Never crash
|
||||
|
||||
People trust SlideCraft with their talks; a crash loses work. **This outranks feature work.**
|
||||
People trust DeckCraft with their talks; a crash loses work. **This outranks feature work.**
|
||||
Standard: craftrules `standards/never-crash.md`.
|
||||
- No `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!` in non-test
|
||||
code; no `unsafe` (`unsafe_code = "forbid"`). Every production crate root carries
|
||||
|
|
@ -118,11 +118,11 @@ Standard: craftrules `standards/never-crash.md`.
|
|||
| L1 | `model` (presentation, masters, layouts, slides, shapes, text, themes, inheritance, text edit ops) |
|
||||
| L2 | `fonts`, `text` (layout), |
|
||||
| L3 | `render` (vello_cpu) |
|
||||
| L4 | `format` (.slidecraft), `pptx` |
|
||||
| L4 | `format` (.deckcraft), `pptx` |
|
||||
| L5 | `engine` (session, history, commands, tools, keys, sample) |
|
||||
| L6 | `mcp` |
|
||||
| L7 | `ui-egui` (swappable; nothing below depends on egui/eframe/winit/rfd) |
|
||||
| apps | `slidecraft` (desktop), `slidecraft-cli`, `slidecraft-web` |
|
||||
| apps | `deckcraft` (desktop), `deckcraft-cli`, `deckcraft-web` |
|
||||
|
||||
- **Everything is a command** (`crates/engine/src/cmd/*`): id, label, ribbon/menu place,
|
||||
shortcut, params doc, `enabled`, `run`, plus tests. Ids follow PowerPoint's ribbon and menus
|
||||
|
|
@ -141,16 +141,16 @@ to `main`.
|
|||
|
||||
## 6. Running and looking at the app
|
||||
|
||||
- `cargo run --release -p slidecraft -- --sample --control 7979` (sample deck + control channel).
|
||||
- `cargo run --release -p deckcraft -- --sample --control 7979` (sample deck + control channel).
|
||||
- Drive it: JSON lines on `127.0.0.1:7979`, e.g.
|
||||
`{"id":1,"method":"engine.execute","params":{"command":"shape.insert","params":{"preset":"star5","rect":[100,100,200,200]}}}`
|
||||
then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods:
|
||||
`crates/ui-egui/src/control.rs`, docs: `docs/control-protocol.md`.
|
||||
- **For UI work, look at the result** (screenshot, read the PNG) and compare with the observations
|
||||
in `plan/powerpoint/01-observed-ui.md`.
|
||||
- Headless: `slidecraft-cli render --sample --all --scale 1 out/`,
|
||||
`slidecraft-cli run --sample --cmd 'slide.new={"title":"Hi"}' --export out.png`.
|
||||
- MCP: `slidecraft-cli mcp` (headless) or `slidecraft-cli mcp --connect 7979` (drive the app).
|
||||
- Headless: `deckcraft-cli render --sample --all --scale 1 out/`,
|
||||
`deckcraft-cli run --sample --cmd 'slide.new={"title":"Hi"}' --export out.png`.
|
||||
- MCP: `deckcraft-cli mcp` (headless) or `deckcraft-cli mcp --connect 7979` (drive the app).
|
||||
See `docs/mcp.md`.
|
||||
- Shell gotcha on the dev machine: `mv`/`cp` are aliased interactive; use `/bin/mv -f`, `/bin/cp -f`.
|
||||
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# Attribution
|
||||
|
||||
Every asset SlideCraft includes, bundles, embeds at build time or shows in its documentation, with
|
||||
Every asset DeckCraft includes, bundles, embeds at build time or shows in its documentation, with
|
||||
its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. `cargo xtask assets`
|
||||
fails if a file under `assets/`, `docs/images/` or `docs/brand/` is missing from this table.
|
||||
|
||||
SlideCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, fonts, themes,
|
||||
DeckCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, fonts, themes,
|
||||
templates or other assets**. Its themes, colour schemes, layouts, preset-shape geometry, sample
|
||||
deck and UI icons are original work of the SlideCraft contributors (drawn or generated in code).
|
||||
deck and UI icons are original work of the DeckCraft contributors (drawn or generated in code).
|
||||
|
||||
## In this repository
|
||||
|
||||
|
|
@ -17,12 +17,12 @@ deck and UI icons are original work of the SlideCraft contributors (drawn or gen
|
|||
| `docs/brand/artcraft-mark.svg`, `docs/brand/artcraft-mark.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | README footer, About |
|
||||
| `docs/brand/artcraft-mark-black.svg`, `docs/brand/artcraft-mark-black.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Mark for light backgrounds |
|
||||
| `docs/brand/LICENSE-brand.txt` | ArtCraft Team | craftrules `standards/license-files` | — (licence text) | Terms for the ArtCraft marks |
|
||||
| `assets/app-icon/slidecraft-1024.png`, `assets/app-icon/hicolor/*/apps/ai.storyteller.slidecraft.png` | SlideCraft contributors | rendered by SlideCraft from `assets/app-icon/icon-source.slidecraft` (shapes only) | MIT OR Apache-2.0 | Placeholder app icon (presentation board) until the mascot icon is drawn |
|
||||
| `assets/app-icon/slidecraft.icns`, `assets/app-icon/slidecraft.ico` | SlideCraft contributors | generated from the PNGs above by `packaging/icons.sh` (sips/iconutil, `cargo xtask ico`) | MIT OR Apache-2.0 | macOS and Windows app icons |
|
||||
| `assets/app-icon/icon-source.slidecraft` | SlideCraft contributors | made with `slidecraft-cli run` (see git history) | MIT OR Apache-2.0 | Source of the placeholder icon |
|
||||
| `crates/media/tests/fixtures/tone*` (`tone.mp3`, `.m4a`, `.aac`, `.ogg`, `.opus`, `.flac`, `.wma`, `tone-alac.m4a`) | SlideCraft contributors | synthesised 0.5 s 440 Hz sine, encoded by `crates/media/tests/fixtures/make.sh` (ffmpeg as the encoder only) | CC0-1.0 | Audio decoding tests |
|
||||
| `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp9.webm`, `clip-av1.mkv`) | SlideCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests |
|
||||
| `crates/media/tests/fixtures/make.sh` | SlideCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures |
|
||||
| `assets/app-icon/deckcraft-1024.png`, `assets/app-icon/hicolor/*/apps/ai.storyteller.deckcraft.png` | DeckCraft contributors | rendered by DeckCraft from `assets/app-icon/icon-source.deckcraft` (shapes only) | MIT OR Apache-2.0 | Placeholder app icon (presentation board) until the mascot icon is drawn |
|
||||
| `assets/app-icon/deckcraft.icns`, `assets/app-icon/deckcraft.ico` | DeckCraft contributors | generated from the PNGs above by `packaging/icons.sh` (sips/iconutil, `cargo xtask ico`) | MIT OR Apache-2.0 | macOS and Windows app icons |
|
||||
| `assets/app-icon/icon-source.deckcraft` | DeckCraft contributors | made with `deckcraft-cli run` (see git history) | MIT OR Apache-2.0 | Source of the placeholder icon |
|
||||
| `crates/media/tests/fixtures/tone*` (`tone.mp3`, `.m4a`, `.aac`, `.ogg`, `.opus`, `.flac`, `.wma`, `tone-alac.m4a`) | DeckCraft contributors | synthesised 0.5 s 440 Hz sine, encoded by `crates/media/tests/fixtures/make.sh` (ffmpeg as the encoder only) | CC0-1.0 | Audio decoding tests |
|
||||
| `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp9.webm`, `clip-av1.mkv`) | DeckCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests |
|
||||
| `crates/media/tests/fixtures/make.sh` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures |
|
||||
|
||||
## Bundled through dependencies
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# SlideCraft — working notes for Claude
|
||||
# DeckCraft — working notes for Claude
|
||||
|
||||
Read [AGENTS.md](AGENTS.md) first: its rules (asset policy, clean room, never crash) bind you and
|
||||
win over anything here.
|
||||
|
|
@ -10,8 +10,8 @@ win over anything here.
|
|||
|
||||
## Habits
|
||||
- Every user-visible action is a command with tests (`crates/engine/src/cmd/*`).
|
||||
- Look at UI changes: `slidecraft --sample --control 7979` + `ui.screenshot`, or
|
||||
`slidecraft-cli render`.
|
||||
- Look at UI changes: `deckcraft --sample --control 7979` + `ui.screenshot`, or
|
||||
`deckcraft-cli render`.
|
||||
- Commit after each landed arc (`M<n>.<k>: what`) and push to `origin main`.
|
||||
- Keep `ROADMAP.md` and `plan/STATUS.md` current.
|
||||
- Don't ask the user questions; record genuinely-theirs decisions in `plan/STATUS.md`.
|
||||
|
|
|
|||
|
|
@ -864,6 +864,302 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"cpal",
|
||||
"deckcraft-engine",
|
||||
"deckcraft-media",
|
||||
"deckcraft-ui-egui",
|
||||
"eframe",
|
||||
"egui",
|
||||
"image",
|
||||
"log",
|
||||
"rfd",
|
||||
"serde_json",
|
||||
"winit",
|
||||
"winresource",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-anim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-geom",
|
||||
"deckcraft-model",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-av1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-bitstream"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-mcp",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-color"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-fonts",
|
||||
"deckcraft-format",
|
||||
"deckcraft-geom",
|
||||
"deckcraft-media",
|
||||
"deckcraft-model",
|
||||
"deckcraft-pdf",
|
||||
"deckcraft-pptx",
|
||||
"deckcraft-render",
|
||||
"deckcraft-text",
|
||||
"i_overlay",
|
||||
"image",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-fonts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"epaint_default_fonts",
|
||||
"harfrust 0.13.3",
|
||||
"kurbo",
|
||||
"log",
|
||||
"skrifa 0.47.0",
|
||||
"unicode-bidi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-format"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-model",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-geom"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-h264"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-hevc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-isobmff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-matroska"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-mcp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-render",
|
||||
"log",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-media"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-av1",
|
||||
"deckcraft-h264",
|
||||
"deckcraft-hevc",
|
||||
"deckcraft-isobmff",
|
||||
"deckcraft-matroska",
|
||||
"deckcraft-ogg",
|
||||
"deckcraft-opus",
|
||||
"deckcraft-vp9",
|
||||
"image",
|
||||
"log",
|
||||
"symphonia",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-model"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-geom",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ogg"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-opus"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pdf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-fonts",
|
||||
"deckcraft-model",
|
||||
"deckcraft-render",
|
||||
"deckcraft-text",
|
||||
"krilla",
|
||||
"kurbo",
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pptx"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-engine",
|
||||
"deckcraft-geom",
|
||||
"deckcraft-model",
|
||||
"image",
|
||||
"log",
|
||||
"quick-xml 0.38.4",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-render"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-fonts",
|
||||
"deckcraft-geom",
|
||||
"deckcraft-model",
|
||||
"deckcraft-text",
|
||||
"image",
|
||||
"kurbo",
|
||||
"log",
|
||||
"vello_common 0.2.0",
|
||||
"vello_cpu 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-text"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-fonts",
|
||||
"deckcraft-geom",
|
||||
"deckcraft-model",
|
||||
"unicode-linebreak",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ui-egui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-anim",
|
||||
"deckcraft-color",
|
||||
"deckcraft-engine",
|
||||
"deckcraft-fonts",
|
||||
"deckcraft-geom",
|
||||
"deckcraft-media",
|
||||
"deckcraft-model",
|
||||
"deckcraft-render",
|
||||
"deckcraft-text",
|
||||
"egui",
|
||||
"egui_kittest",
|
||||
"js-sys",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-vp9"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-web"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-ui-egui",
|
||||
"eframe",
|
||||
"egui",
|
||||
"js-sys",
|
||||
"log",
|
||||
"rfd",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.5.0"
|
||||
|
|
@ -3447,302 +3743,6 @@ version = "0.4.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"cpal",
|
||||
"eframe",
|
||||
"egui",
|
||||
"image",
|
||||
"log",
|
||||
"rfd",
|
||||
"serde_json",
|
||||
"slidecraft-engine",
|
||||
"slidecraft-media",
|
||||
"slidecraft-ui-egui",
|
||||
"winit",
|
||||
"winresource",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-anim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"slidecraft-geom",
|
||||
"slidecraft-model",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-av1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
"slidecraft-bitstream",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-bitstream"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"slidecraft-engine",
|
||||
"slidecraft-mcp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-color"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"i_overlay",
|
||||
"image",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slidecraft-color",
|
||||
"slidecraft-fonts",
|
||||
"slidecraft-format",
|
||||
"slidecraft-geom",
|
||||
"slidecraft-media",
|
||||
"slidecraft-model",
|
||||
"slidecraft-pdf",
|
||||
"slidecraft-pptx",
|
||||
"slidecraft-render",
|
||||
"slidecraft-text",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-fonts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"epaint_default_fonts",
|
||||
"harfrust 0.13.3",
|
||||
"kurbo",
|
||||
"log",
|
||||
"skrifa 0.47.0",
|
||||
"unicode-bidi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-format"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slidecraft-model",
|
||||
"thiserror 2.0.21",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-geom"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-h264"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
"slidecraft-bitstream",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-hevc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
"slidecraft-bitstream",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-isobmff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"slidecraft-bitstream",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-matroska"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-mcp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde_json",
|
||||
"slidecraft-engine",
|
||||
"slidecraft-render",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-media"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"slidecraft-av1",
|
||||
"slidecraft-h264",
|
||||
"slidecraft-hevc",
|
||||
"slidecraft-isobmff",
|
||||
"slidecraft-matroska",
|
||||
"slidecraft-ogg",
|
||||
"slidecraft-opus",
|
||||
"slidecraft-vp9",
|
||||
"symphonia",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-model"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slidecraft-color",
|
||||
"slidecraft-geom",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-ogg"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-opus"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-pdf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"krilla",
|
||||
"kurbo",
|
||||
"log",
|
||||
"serde",
|
||||
"slidecraft-fonts",
|
||||
"slidecraft-model",
|
||||
"slidecraft-render",
|
||||
"slidecraft-text",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-pptx"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"quick-xml 0.38.4",
|
||||
"serde_json",
|
||||
"slidecraft-color",
|
||||
"slidecraft-engine",
|
||||
"slidecraft-geom",
|
||||
"slidecraft-model",
|
||||
"thiserror 2.0.21",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-render"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo",
|
||||
"log",
|
||||
"slidecraft-color",
|
||||
"slidecraft-fonts",
|
||||
"slidecraft-geom",
|
||||
"slidecraft-model",
|
||||
"slidecraft-text",
|
||||
"vello_common 0.2.0",
|
||||
"vello_cpu 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-text"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"slidecraft-color",
|
||||
"slidecraft-fonts",
|
||||
"slidecraft-geom",
|
||||
"slidecraft-model",
|
||||
"unicode-linebreak",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-ui-egui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_kittest",
|
||||
"js-sys",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slidecraft-anim",
|
||||
"slidecraft-color",
|
||||
"slidecraft-engine",
|
||||
"slidecraft-fonts",
|
||||
"slidecraft-geom",
|
||||
"slidecraft-media",
|
||||
"slidecraft-model",
|
||||
"slidecraft-render",
|
||||
"slidecraft-text",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-vp9"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
"slidecraft-bitstream",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slidecraft-web"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"js-sys",
|
||||
"log",
|
||||
"rfd",
|
||||
"serde_json",
|
||||
"slidecraft-engine",
|
||||
"slidecraft-ui-egui",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slotmap"
|
||||
version = "1.1.1"
|
||||
|
|
|
|||
50
Cargo.toml
|
|
@ -1,41 +1,41 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/slidecraft", "apps/slidecraft-cli"]
|
||||
default-members = ["crates/*", "apps/deckcraft", "apps/deckcraft-cli"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.90"
|
||||
repository = "https://github.com/storytold/slidecraft"
|
||||
repository = "https://github.com/storytold/deckcraft"
|
||||
|
||||
[workspace.dependencies]
|
||||
# internal
|
||||
slidecraft-geom = { path = "crates/geom" }
|
||||
slidecraft-color = { path = "crates/color" }
|
||||
slidecraft-model = { path = "crates/model" }
|
||||
slidecraft-fonts = { path = "crates/fonts" }
|
||||
slidecraft-text = { path = "crates/text" }
|
||||
slidecraft-render = { path = "crates/render" }
|
||||
slidecraft-pdf = { path = "crates/pdf" }
|
||||
slidecraft-pptx = { path = "crates/pptx" }
|
||||
slidecraft-format = { path = "crates/format" }
|
||||
slidecraft-anim = { path = "crates/anim" }
|
||||
slidecraft-engine = { path = "crates/engine" }
|
||||
slidecraft-mcp = { path = "crates/mcp" }
|
||||
slidecraft-ui-egui = { path = "crates/ui-egui" }
|
||||
slidecraft-media = { path = "crates/media" }
|
||||
deckcraft-geom = { path = "crates/geom" }
|
||||
deckcraft-color = { path = "crates/color" }
|
||||
deckcraft-model = { path = "crates/model" }
|
||||
deckcraft-fonts = { path = "crates/fonts" }
|
||||
deckcraft-text = { path = "crates/text" }
|
||||
deckcraft-render = { path = "crates/render" }
|
||||
deckcraft-pdf = { path = "crates/pdf" }
|
||||
deckcraft-pptx = { path = "crates/pptx" }
|
||||
deckcraft-format = { path = "crates/format" }
|
||||
deckcraft-anim = { path = "crates/anim" }
|
||||
deckcraft-engine = { path = "crates/engine" }
|
||||
deckcraft-mcp = { path = "crates/mcp" }
|
||||
deckcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
deckcraft-media = { path = "crates/media" }
|
||||
# codecs (ported from FilmCraft, same owner and licence)
|
||||
slidecraft-bitstream = { path = "crates/bitstream" }
|
||||
slidecraft-h264 = { path = "crates/h264" }
|
||||
slidecraft-hevc = { path = "crates/hevc" }
|
||||
slidecraft-vp9 = { path = "crates/vp9" }
|
||||
slidecraft-av1 = { path = "crates/av1" }
|
||||
slidecraft-isobmff = { path = "crates/isobmff" }
|
||||
slidecraft-matroska = { path = "crates/matroska" }
|
||||
slidecraft-ogg = { path = "crates/ogg" }
|
||||
slidecraft-opus = { path = "crates/opus" }
|
||||
deckcraft-bitstream = { path = "crates/bitstream" }
|
||||
deckcraft-h264 = { path = "crates/h264" }
|
||||
deckcraft-hevc = { path = "crates/hevc" }
|
||||
deckcraft-vp9 = { path = "crates/vp9" }
|
||||
deckcraft-av1 = { path = "crates/av1" }
|
||||
deckcraft-isobmff = { path = "crates/isobmff" }
|
||||
deckcraft-matroska = { path = "crates/matroska" }
|
||||
deckcraft-ogg = { path = "crates/ogg" }
|
||||
deckcraft-opus = { path = "crates/opus" }
|
||||
|
||||
# foundation
|
||||
kurbo = { version = "0.13", features = ["serde"] }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 ArtCraft Team and the SlideCraft contributors
|
||||
Copyright (c) 2026 ArtCraft Team and the DeckCraft contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
4
NOTICE
|
|
@ -1,5 +1,5 @@
|
|||
SlideCraft
|
||||
Copyright (c) 2026 ArtCraft Team and the SlideCraft contributors.
|
||||
DeckCraft
|
||||
Copyright (c) 2026 ArtCraft Team and the DeckCraft contributors.
|
||||
Licensed under the MIT License or the Apache License, Version 2.0, at your option
|
||||
(LICENSE-MIT, LICENSE-APACHE).
|
||||
|
||||
|
|
|
|||
40
README.md
|
|
@ -7,7 +7,7 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">SlideCraft</h1>
|
||||
<h1 align="center">DeckCraft</h1>
|
||||
|
||||
<p align="center">
|
||||
<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 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/apps">All Crafting Apps</a>
|
||||
</p>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
|
||||
## What works today
|
||||
|
||||
SlideCraft is in early development. Today:
|
||||
DeckCraft is in early development. Today:
|
||||
|
||||
- **Presentation model** with slide masters, eleven layouts, placeholders that inherit position and
|
||||
formatting, themes (colour, font and effect schemes), sections, notes, comments, transitions and
|
||||
|
|
@ -70,25 +70,25 @@ SlideCraft is in early development. Today:
|
|||
## Getting started
|
||||
|
||||
```sh
|
||||
git clone https://github.com/storytold/slidecraft
|
||||
cd slidecraft
|
||||
cargo run --release -p slidecraft-cli -- render --sample --all --scale 1 out/
|
||||
git clone https://github.com/storytold/deckcraft
|
||||
cd deckcraft
|
||||
cargo run --release -p deckcraft-cli -- render --sample --all --scale 1 out/
|
||||
```
|
||||
|
||||
Fonts come from [craft-fonts](https://github.com/storytold/craft-fonts): clone it next to this repo
|
||||
(`../craft-fonts`) and local builds pick it up automatically; without it SlideCraft uses your system
|
||||
(`../craft-fonts`) and local builds pick it up automatically; without it DeckCraft uses your system
|
||||
fonts.
|
||||
|
||||
## Agents, CLI and MCP
|
||||
|
||||
Every action in SlideCraft is a command with a stable id, so people, scripts and AI agents use the
|
||||
Every action in DeckCraft is a command with a stable id, so people, scripts and AI agents use the
|
||||
same verbs:
|
||||
|
||||
```sh
|
||||
slidecraft-cli commands format. # list commands
|
||||
slidecraft-cli run --sample --cmd 'slide.new={"layout":"titleOnly","title":"Hello"}' --save hello.slidecraft
|
||||
slidecraft-cli mcp # MCP server over stdio (headless)
|
||||
slidecraft-cli mcp --connect 7979 # drive the running app (slidecraft --control 7979)
|
||||
deckcraft-cli commands format. # list commands
|
||||
deckcraft-cli run --sample --cmd 'slide.new={"layout":"titleOnly","title":"Hello"}' --save hello.deckcraft
|
||||
deckcraft-cli mcp # MCP server over stdio (headless)
|
||||
deckcraft-cli mcp --connect 7979 # drive the running app (deckcraft --control 7979)
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
|
@ -97,7 +97,7 @@ See [ROADMAP.md](ROADMAP.md) for milestones, parity estimates and what's next.
|
|||
|
||||
## The Crafting Apps
|
||||
|
||||
SlideCraft is one of the **Crafting Apps**: free, open-source creative tools from the
|
||||
DeckCraft is one of the **Crafting Apps**: free, open-source creative tools from the
|
||||
[ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to
|
||||
stand on its own.
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ stand on its own.
|
|||
| <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/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.
|
||||
|
||||
|
|
@ -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://getartcraft.com/">getartcraft.com</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>
|
||||
|
||||
## License and credits
|
||||
|
||||
SlideCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
|
||||
Copyright (c) 2026 ArtCraft Team and the SlideCraft contributors. Required notices are in [NOTICE](NOTICE).
|
||||
DeckCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
|
||||
Copyright (c) 2026 ArtCraft Team and the DeckCraft contributors. Required notices are in [NOTICE](NOTICE).
|
||||
|
||||
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
|
||||
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md).
|
||||
|
||||
SlideCraft's themes, layouts, shape geometry, icons and sample decks are original work, drawn or
|
||||
DeckCraft's themes, layouts, shape geometry, icons and sample decks are original work, drawn or
|
||||
generated in code; no Microsoft artwork, fonts, themes or templates are used.
|
||||
|
||||
The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the
|
||||
ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
|
||||
part of this repository and SlideCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
|
||||
part of this repository and DeckCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
|
||||
Forks and modified versions must remove them.
|
||||
|
||||
<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">
|
||||
<a href="https://getartcraft.com/"><img alt="ArtCraft" src="docs/brand/artcraft-mark.svg" width="28"></a><br>
|
||||
|
|
|
|||
73
ROADMAP.md
|
|
@ -1,6 +1,6 @@
|
|||
# SlideCraft roadmap
|
||||
# DeckCraft roadmap
|
||||
|
||||
SlideCraft aims at full PowerPoint parity — and to be better: faster, open (a documented zip+JSON
|
||||
DeckCraft aims at full PowerPoint parity — and to be better: faster, open (a documented zip+JSON
|
||||
format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
|
||||
(macOS, Windows, Linux, BSD and the web).
|
||||
|
||||
|
|
@ -34,46 +34,67 @@ format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and av
|
|||
- PDF export: slides, notes pages and handouts (1–9 per page) with a selectable real-text layer, hyperlinks and slide bookmarks (`file.export {format: "pdf", layout}`, File › Export…).
|
||||
- UI: PowerPoint-style ribbon with contextual tabs, ~240 original icons, status bar, panes, command
|
||||
palette, light/dark.
|
||||
- Automation: ~200 commands, every one reachable from `slidecraft-cli`, the app's JSON control
|
||||
- Automation: ~200 commands, every one reachable from `deckcraft-cli`, the app's JSON control
|
||||
channel and the MCP server (headless or connected to the running app).
|
||||
- Web: `apps/slidecraft-web` runs the same UI in the browser (trunk; WebGPU with WebGL2 fallback),
|
||||
- Web: `apps/deckcraft-web` runs the same UI in the browser (trunk; WebGPU with WebGL2 fallback),
|
||||
opening the sample deck; Open/Insert use the browser file picker, Save/Export download.
|
||||
- Release CI: pushes to `release` build a draft GitHub Release (macOS universal dmg, Windows
|
||||
x64/x86 msi + zip, Linux AppImage/deb/rpm/tar.gz + Flatpak, FreeBSD tar.gz, web zip); signing
|
||||
secrets live in the `release` environment. Version: `cargo xtask version`.
|
||||
|
||||
**In progress:** PPTX import/export · first signed release run.
|
||||
|
||||
**Next (in order):** vector PDF artwork · WMA/WMV decoding · Format Shape pane depth · connectors with
|
||||
glue, freeform and edit points · merge shapes · Animation Pane polish · native macOS menu bar ·
|
||||
print · Notes/Handout masters · equations · SVG pictures.
|
||||
**Next (in order):** first signed release run · PPTX corpus hardening against real-world decks ·
|
||||
Animation Pane and presenter view polish · vector PDF artwork · Format Shape pane depth (3-D,
|
||||
picture/texture options) · edit points · native macOS menu bar · print · Notes/Handout masters ·
|
||||
equations · SVG pictures · WMA/WMV decoding.
|
||||
|
||||
Milestone details live in `plan/execution-plan.md` (M0–M14, local planning notes).
|
||||
|
||||
## How close to an alpha (estimate, 2026-10-07)
|
||||
|
||||
An **alpha** here means: someone can install a signed build on macOS, Windows or Linux, make a real
|
||||
deck from scratch or from a PowerPoint file, present it with transitions, animations and media,
|
||||
save it back to `.pptx`/PDF without losing work, and hit no crashes on the common paths.
|
||||
|
||||
**DeckCraft is about 80% of the way to that alpha — roughly 30–40 wall-clock hours of a single
|
||||
Claude Opus 5.5 agent** (about 12–18 hours with three agents in parallel).
|
||||
|
||||
| Alpha blocker | State | Estimate |
|
||||
|---|---|---|
|
||||
| First real release run: signing, notarization, installers verified on each OS | Workflows written and linted, never run | 6 h |
|
||||
| PPTX fidelity on a corpus of real decks (import, round-trip, opens without repair) | Verified on generated decks only | 8 h |
|
||||
| Presenter view and Animation Pane polish (timeline, reorder, preview) | Working, rough | 6 h |
|
||||
| Soak and fuzz the editor (random command sequences, big decks, undo/redo) for crashes and slowness | Unit tests and guards only | 5 h |
|
||||
| UI fidelity pass on the most-used ribbon groups and dialogs; first-run experience | Mostly there | 6 h |
|
||||
| Mascot app icon (owner), README/site screenshots, user docs | Placeholder icon | 3 h |
|
||||
|
||||
Already alpha-ready: editing, text, shapes (incl. connectors, freeform, merge), themes and
|
||||
backgrounds, tables, charts, transitions, animations, slide show, audio/video playback, PDF export,
|
||||
AutoRecover, CLI/MCP automation, web build.
|
||||
|
||||
## How far from full parity (estimate, 2026-10-07)
|
||||
|
||||
**Breadth: ~74% weighted** (P0 core 88%, P1 67%, P2 33%) over the 187 features of the PowerPoint
|
||||
catalogue, scored row by row in [docs/parity.md](docs/parity.md) (`cargo xtask parity` recomputes
|
||||
it). Many features scored done still lack some of PowerPoint's options, dialogs or pixel fidelity, so
|
||||
**overall parity including depth is about 55%**.
|
||||
**Breadth: ~79% weighted** (P0 core 93%, P1 74%, P2 35%) over the 187 features of the PowerPoint catalogue, scored row by
|
||||
row in [docs/parity.md](docs/parity.md) (`cargo xtask parity` recomputes it). Many features scored
|
||||
done still lack some of PowerPoint's options, dialogs or pixel fidelity, so **overall parity
|
||||
including depth is about 62%**.
|
||||
|
||||
**Remaining work: about 230 wall-clock hours of a single Claude Opus 5.5 agent** (±30%), or roughly
|
||||
80–110 hours with four agents in parallel on separate crates:
|
||||
**Remaining work to 100%: about 190 wall-clock hours of a single Claude Opus 5.5 agent** (±30%),
|
||||
or roughly 65–90 hours with four agents in parallel on separate crates:
|
||||
|
||||
| Work | Estimate |
|
||||
|---|---|
|
||||
| Open P0 (16 partial, 1 missing: PPTX fidelity, vector PDF, media playback, Format Shape pane, presenter view…) | 30 h |
|
||||
| Open P1 (23 partial, 10 missing: connectors/glue, freeform, merge shapes, print, multi-monitor, SVG…) | 45 h |
|
||||
| Open P2 (11 partial, 28 missing: equations, video export, 3-D, remove background, thesaurus…) | 40 h |
|
||||
| Depth and pixel fidelity of every ribbon group, dialog and pane against PowerPoint | 75 h |
|
||||
| Performance (incremental rendering, GPU raster) and PPTX corpus hardening | 30 h |
|
||||
| Platform verification and release hardening (signing, Flatpak, BSD, web) | 10 h |
|
||||
| Alpha blockers above | 35 h |
|
||||
| Open P0/P1 rows (vector PDF, print, edit points, SVG, multi-monitor, draw table, SmartArt text pane, Notes/Handout masters…) | 45 h |
|
||||
| Open P2 rows (equations, video export, 3-D, remove background, thesaurus, compare, record show…) | 40 h |
|
||||
| Depth and pixel fidelity of every ribbon group, dialog and pane against PowerPoint | 50 h |
|
||||
| Performance (incremental rendering, GPU raster, streaming media decode) | 20 h |
|
||||
|
||||
Basis: the M0 arc (model, renderer, text engine, engine with ~200 commands, ribbon UI, show engine,
|
||||
CLI and MCP) took roughly 25 wall-clock agent hours; open catalogue rows average 1–2 hours each.
|
||||
Basis: in this session the engine, renderer, UI, show engine, PPTX, PDF, media and release
|
||||
pipeline were built in about 30 wall-clock hours with up to three agents in parallel; recent
|
||||
catalogue rows (connectors, freeform, merge shapes, AutoRecover, gradients) took 1–2 hours each.
|
||||
|
||||
## Agents: CLI and MCP
|
||||
|
||||
Every command is reachable from `slidecraft-cli` (`run`, `describe`, `commands`, `app` for the running
|
||||
window, `render`, `convert`), from MCP (`slidecraft-cli mcp`, optionally `--connect PORT`), and from the
|
||||
app's JSON control channel (`slidecraft --control PORT`).
|
||||
Every command is reachable from `deckcraft-cli` (`run`, `describe`, `commands`, `app` for the running
|
||||
window, `render`, `convert`), from MCP (`deckcraft-cli mcp`, optionally `--connect PORT`), and from the
|
||||
app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
//! Headless SlideCraft.
|
||||
//! Headless DeckCraft.
|
||||
//!
|
||||
//! ```text
|
||||
//! slidecraft-cli info FILE # slides, titles, layouts (JSON)
|
||||
//! slidecraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] OUT.png|OUT_DIR
|
||||
//! slidecraft-cli convert IN OUT # .slidecraft ⇄ .pptx, outline .txt, .png (first slide)
|
||||
//! slidecraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print]
|
||||
//! slidecraft-cli commands [FILTER] # list commands (JSON)
|
||||
//! slidecraft-cli describe ID # one command
|
||||
//! slidecraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] # drive the running app
|
||||
//! slidecraft-cli mcp [--connect PORT] [--sample] # MCP server over stdio
|
||||
//! slidecraft-cli links | --version
|
||||
//! deckcraft-cli info FILE # slides, titles, layouts (JSON)
|
||||
//! deckcraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] OUT.png|OUT_DIR
|
||||
//! deckcraft-cli convert IN OUT # .deckcraft ⇄ .pptx, outline .txt, .png (first slide)
|
||||
//! deckcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print]
|
||||
//! deckcraft-cli commands [FILTER] # list commands (JSON)
|
||||
//! deckcraft-cli describe ID # one command
|
||||
//! deckcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON] # drive the running app
|
||||
//! deckcraft-cli mcp [--connect PORT] [--sample] # MCP server over stdio
|
||||
//! deckcraft-cli links | --version
|
||||
//! ```
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use deckcraft_engine::Session;
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_engine::Session;
|
||||
|
||||
macro_rules! outln {
|
||||
($($arg:tt)*) => {{
|
||||
|
|
@ -26,28 +26,28 @@ macro_rules! outln {
|
|||
if e.kind() == std::io::ErrorKind::BrokenPipe {
|
||||
std::process::exit(0);
|
||||
}
|
||||
eprintln!("slidecraft-cli: can't write to stdout: {e}");
|
||||
eprintln!("deckcraft-cli: can't write to stdout: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
const USAGE: &str = "usage: slidecraft-cli info FILE
|
||||
slidecraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] OUT
|
||||
slidecraft-cli convert IN OUT
|
||||
slidecraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print]
|
||||
slidecraft-cli commands [FILTER]
|
||||
slidecraft-cli describe COMMAND
|
||||
slidecraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]
|
||||
slidecraft-cli mcp [--connect PORT] [--sample]
|
||||
slidecraft-cli links | --version";
|
||||
const USAGE: &str = "usage: deckcraft-cli info FILE
|
||||
deckcraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] OUT
|
||||
deckcraft-cli convert IN OUT
|
||||
deckcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print]
|
||||
deckcraft-cli commands [FILTER]
|
||||
deckcraft-cli describe COMMAND
|
||||
deckcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]
|
||||
deckcraft-cli mcp [--connect PORT] [--sample]
|
||||
deckcraft-cli links | --version";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let rest = args.get(1..).unwrap_or(&[]);
|
||||
match args.first().map(String::as_str) {
|
||||
Some("--version" | "-V" | "version") => {
|
||||
outln!("slidecraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
outln!("deckcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Some("info") => report(info(rest)),
|
||||
|
|
@ -70,13 +70,13 @@ fn main() -> ExitCode {
|
|||
Some("app") => report(app(rest)),
|
||||
Some("mcp") => report(mcp(rest)),
|
||||
Some("links") => {
|
||||
use slidecraft_engine::links::*;
|
||||
use deckcraft_engine::links::*;
|
||||
outln!("Discord {DISCORD}\nWebsite {WEBSITE}\nApp page {APP_PAGE}\nGitHub {GITHUB}");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
_ => {
|
||||
eprintln!("{USAGE}");
|
||||
eprintln!("\nCommunity: {} · {}", slidecraft_engine::links::DISCORD, slidecraft_engine::links::APP_PAGE);
|
||||
eprintln!("\nCommunity: {} · {}", deckcraft_engine::links::DISCORD, deckcraft_engine::links::APP_PAGE);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ fn report(r: Result<(), String>) -> ExitCode {
|
|||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("slidecraft-cli: {e}");
|
||||
eprintln!("deckcraft-cli: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ fn report(r: Result<(), String>) -> ExitCode {
|
|||
|
||||
fn open(s: &mut Session, input: Option<&str>, sample: bool) -> Result<(), String> {
|
||||
if sample {
|
||||
return slidecraft_engine::sample::open_sample(s).map_err(|e| e.to_string());
|
||||
return deckcraft_engine::sample::open_sample(s).map_err(|e| e.to_string());
|
||||
}
|
||||
match input {
|
||||
Some(p) => s.execute("file.open", &json!({"path": p})).map(|_| ()).map_err(|e| e.to_string()),
|
||||
|
|
@ -153,7 +153,7 @@ fn render(args: &[String]) -> Result<(), String> {
|
|||
if i >= n {
|
||||
return Err(format!("no slide {i} (the deck has {n})"));
|
||||
}
|
||||
let (png, w, h) = slidecraft_engine::cmd::file::render_png(&st.doc, i, scale, edit);
|
||||
let (png, w, h) = deckcraft_engine::cmd::file::render_png(&st.doc, i, scale, edit);
|
||||
let path = if all { format!("{out}/slide-{:02}.png", i + 1) } else { out.clone() };
|
||||
std::fs::write(&path, png).map_err(|e| format!("{path}: {e}"))?;
|
||||
outln!("{path} ({w}×{h})");
|
||||
|
|
@ -165,7 +165,7 @@ fn convert(args: &[String]) -> Result<(), String> {
|
|||
let (Some(input), Some(out)) = (args.first(), args.get(1)) else { return Err("convert needs IN and OUT".into()) };
|
||||
let mut s = Session::new();
|
||||
open(&mut s, Some(input), false)?;
|
||||
let fmt = slidecraft_engine::cmd::file::format_for_path(out);
|
||||
let fmt = deckcraft_engine::cmd::file::format_for_path(out);
|
||||
let r = if matches!(fmt, "png" | "jpeg") {
|
||||
s.execute("file.export", &json!({"path": out, "slide": 0}))
|
||||
} else {
|
||||
|
|
@ -218,14 +218,14 @@ fn run(args: &[String]) -> Result<(), String> {
|
|||
|
||||
fn describe(id: Option<&str>) -> Result<(), String> {
|
||||
let id = id.ok_or("describe needs a command id")?;
|
||||
let c = slidecraft_engine::find_command(id).ok_or_else(|| format!("unknown command `{id}`"))?;
|
||||
let c = deckcraft_engine::find_command(id).ok_or_else(|| format!("unknown command `{id}`"))?;
|
||||
outln!("{} — {}\n where: {}\n shortcut: {}\n params: {}", c.id, c.label, c.menu.join(" ▸ "), c.shortcut.unwrap_or("—"), c.params);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn app(args: &[String]) -> Result<(), String> {
|
||||
use slidecraft_mcp::{Backend, Remote, control_addr};
|
||||
let mut port = slidecraft_mcp::DEFAULT_PORT.to_string();
|
||||
use deckcraft_mcp::{Backend, Remote, control_addr};
|
||||
let mut port = deckcraft_mcp::DEFAULT_PORT.to_string();
|
||||
let mut method: Option<String> = None;
|
||||
let mut rest = vec![];
|
||||
let mut it = args.iter();
|
||||
|
|
@ -237,7 +237,7 @@ fn app(args: &[String]) -> Result<(), String> {
|
|||
}
|
||||
}
|
||||
let mut r = Remote::connect(&control_addr(&port))
|
||||
.map_err(|e| format!("no SlideCraft app on port {port} ({e}); start it with `slidecraft --control {port}`"))?;
|
||||
.map_err(|e| format!("no DeckCraft app on port {port} ({e}); start it with `deckcraft --control {port}`"))?;
|
||||
let parse = |s: Option<&String>| -> Result<Value, String> {
|
||||
s.map(|j| serde_json::from_str(j).map_err(|e| format!("bad JSON: {e}"))).transpose().map(|v| v.unwrap_or(json!({})))
|
||||
};
|
||||
|
|
@ -253,7 +253,7 @@ fn app(args: &[String]) -> Result<(), String> {
|
|||
}
|
||||
|
||||
fn mcp(args: &[String]) -> Result<(), String> {
|
||||
use slidecraft_mcp::{Backend, Headless, Remote, Server, control_addr};
|
||||
use deckcraft_mcp::{Backend, Headless, Remote, Server, control_addr};
|
||||
let mut connect: Option<String> = None;
|
||||
let mut sample = false;
|
||||
let mut it = args.iter();
|
||||
|
|
@ -269,7 +269,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
|
|||
None => {
|
||||
let mut h = Headless::new();
|
||||
if sample {
|
||||
slidecraft_engine::sample::open_sample(&mut h.session).map_err(|e| e.to_string())?;
|
||||
deckcraft_engine::sample::open_sample(&mut h.session).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Box::new(h)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "slidecraft-web"
|
||||
description = "SlideCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
|
||||
name = "deckcraft-web"
|
||||
description = "DeckCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
|
@ -10,12 +10,12 @@ publish = false
|
|||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "slidecraft-web"
|
||||
name = "deckcraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
slidecraft-engine = { workspace = true }
|
||||
slidecraft-ui-egui = { workspace = true }
|
||||
deckcraft-engine = { workspace = true }
|
||||
deckcraft-ui-egui = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Build with: trunk build --release (from apps/slidecraft-web)
|
||||
# Build with: trunk build --release (from apps/deckcraft-web)
|
||||
[build]
|
||||
target = "index.html"
|
||||
dist = "../../dist/web"
|
||||
|
|
@ -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…</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//! SlideCraft in the browser.
|
||||
//! DeckCraft in the browser.
|
||||
//!
|
||||
//! Runs the same [`slidecraft_ui_egui::SlideApp`] as the desktop app through eframe's web
|
||||
//! Runs the same [`deckcraft_ui_egui::SlideApp`] as the desktop app through eframe's web
|
||||
//! runner (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release`
|
||||
//! from this directory (output in `dist/web`).
|
||||
//!
|
||||
|
|
@ -27,5 +27,5 @@ fn main() {
|
|||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() {
|
||||
eprintln!("slidecraft-web only runs in the browser: build it with `trunk build --release` in apps/slidecraft-web");
|
||||
eprintln!("deckcraft-web only runs in the browser: build it with `trunk build --release` in apps/deckcraft-web");
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
//! The browser shell: web `Services`, drag-and-drop, and the eframe web runner.
|
||||
|
||||
use slidecraft_engine::Session;
|
||||
use slidecraft_ui_egui::{Inbox, Services, SlideApp};
|
||||
use deckcraft_engine::Session;
|
||||
use deckcraft_ui_egui::{Inbox, Services, SlideApp};
|
||||
use wasm_bindgen::JsCast as _;
|
||||
|
||||
const PRESENTATION_EXTS: &[&str] = &["slidecraft", "pptx", "potx", "ppsx", "txt", "md"];
|
||||
const PRESENTATION_EXTS: &[&str] = &["deckcraft", "pptx", "potx", "ppsx", "txt", "md"];
|
||||
const PICTURE_EXTS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff"];
|
||||
const AUDIO_EXTS: &[&str] = &["wav", "mp3", "m4a", "aac", "flac", "ogg", "opus", "aif", "aiff"];
|
||||
const VIDEO_EXTS: &[&str] = &["mp4", "m4v", "mov", "webm"];
|
||||
const CANVAS_ID: &str = "slidecraft_canvas";
|
||||
const LOADING_ID: &str = "slidecraft_loading";
|
||||
const CANVAS_ID: &str = "deckcraft_canvas";
|
||||
const LOADING_ID: &str = "deckcraft_loading";
|
||||
|
||||
pub fn start() {
|
||||
eframe::WebLogger::init(log::LevelFilter::Info).ok();
|
||||
|
|
@ -34,13 +34,13 @@ pub fn start() {
|
|||
options,
|
||||
Box::new(move |cc| {
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
log::info!("slidecraft-web {}: wgpu backend {:?}", env!("CARGO_PKG_VERSION"), rs.adapter.get_info().backend);
|
||||
log::info!("deckcraft-web {}: wgpu backend {:?}", env!("CARGO_PKG_VERSION"), rs.adapter.get_info().backend);
|
||||
}
|
||||
let inbox: Inbox = Inbox::default();
|
||||
let mut app = SlideApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone()));
|
||||
let q = query();
|
||||
if !q.contains("blank") {
|
||||
if let Err(e) = slidecraft_engine::sample::open_sample(&mut app.session) {
|
||||
if let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) {
|
||||
log::error!("sample deck: {e}");
|
||||
}
|
||||
} else if let Err(e) = app.run("file.new", serde_json::json!({})) {
|
||||
|
|
@ -56,7 +56,7 @@ pub fn start() {
|
|||
if let Some(el) = document.get_element_by_id(LOADING_ID) {
|
||||
match result {
|
||||
Ok(()) => el.remove(),
|
||||
Err(e) => el.set_inner_html(&format!("<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`.
|
||||
fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
let js = |e: wasm_bindgen::JsValue| format!("{e:?}");
|
||||
let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "slidecraft".into());
|
||||
let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "deckcraft".into());
|
||||
let window = web_sys::window().ok_or("no window")?;
|
||||
let document = window.document().ok_or("no document")?;
|
||||
let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes));
|
||||
|
|
@ -170,7 +170,7 @@ fn mime_for(name: &str) -> &'static str {
|
|||
Some("pdf") => "application/pdf",
|
||||
Some("txt") => "text/plain",
|
||||
Some("pptx") => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
Some("slidecraft") => "application/zip",
|
||||
Some("deckcraft") => "application/zip",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,26 @@
|
|||
[package]
|
||||
name = "slidecraft"
|
||||
name = "deckcraft"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "SlideCraft desktop app"
|
||||
description = "DeckCraft desktop app"
|
||||
publish = false
|
||||
default-run = "slidecraft"
|
||||
default-run = "deckcraft"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
slidecraft-engine = { workspace = true }
|
||||
slidecraft-ui-egui = { workspace = true }
|
||||
slidecraft-media = { workspace = true }
|
||||
deckcraft-engine = { workspace = true }
|
||||
deckcraft-ui-egui = { workspace = true }
|
||||
deckcraft-media = { workspace = true }
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] }
|
||||
egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
# Icon + VERSIONINFO for slidecraft.exe; build.rs only uses it when the target is Windows.
|
||||
# Icon + VERSIONINFO for deckcraft.exe; build.rs only uses it when the target is Windows.
|
||||
winresource = "0.1"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
//! cpal audio output for media playback: the device pulls mixed samples from the
|
||||
//! `slidecraft_media::Player`, which makes it the playback clock video frames follow.
|
||||
//! `deckcraft_media::Player`, which makes it the playback clock video frames follow.
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use slidecraft_media::AudioOut;
|
||||
use deckcraft_media::AudioOut;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CpalOut {
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
//! Loopback JSON-lines control server: one request per line, one reply per line.
|
||||
//! This is the transport the MCP server (`slidecraft-cli mcp --connect`) wraps.
|
||||
//! This is the transport the MCP server (`deckcraft-cli mcp --connect`) wraps.
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use std::time::Duration;
|
||||
|
||||
use deckcraft_ui_egui::ControlRequest;
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_ui_egui::ControlRequest;
|
||||
|
||||
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("slidecraft: control server failed to bind 127.0.0.1:{port}: {e}");
|
||||
eprintln!("deckcraft: control server failed to bind 127.0.0.1:{port}: {e}");
|
||||
return rx;
|
||||
}
|
||||
};
|
||||
eprintln!("slidecraft: control server listening on 127.0.0.1:{port}");
|
||||
eprintln!("deckcraft: control server listening on 127.0.0.1:{port}");
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
|
|
@ -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":…}`.
|
||||
//! See `slidecraft_ui_egui::control` for the methods.
|
||||
//! See `deckcraft_ui_egui::control` for the methods.
|
||||
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
|
@ -12,8 +12,8 @@
|
|||
mod audio;
|
||||
mod control_server;
|
||||
|
||||
use slidecraft_engine::Session;
|
||||
use slidecraft_ui_egui::{Services, SlideApp};
|
||||
use deckcraft_engine::Session;
|
||||
use deckcraft_ui_egui::{Services, SlideApp};
|
||||
|
||||
struct App(SlideApp);
|
||||
|
||||
|
|
@ -34,38 +34,38 @@ impl eframe::App for App {
|
|||
|
||||
fn prefs_path() -> Option<std::path::PathBuf> {
|
||||
let base = if cfg!(target_os = "macos") {
|
||||
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/SlideCraft"))
|
||||
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/DeckCraft"))
|
||||
} else if cfg!(windows) {
|
||||
std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("SlideCraft"))
|
||||
std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("DeckCraft"))
|
||||
} else {
|
||||
std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(std::path::PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config")))
|
||||
.map(|c| c.join("slidecraft"))
|
||||
.map(|c| c.join("deckcraft"))
|
||||
};
|
||||
base.map(|b| b.join("ui.json"))
|
||||
}
|
||||
|
||||
fn load_prefs(app: &mut SlideApp) {
|
||||
if std::env::var_os("SLIDECRAFT_NO_PREFS").is_some() {
|
||||
if std::env::var_os("DECKCRAFT_NO_PREFS").is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(p) = prefs_path()
|
||||
&& let Ok(bytes) = std::fs::read(&p)
|
||||
&& let Ok(ui) = serde_json::from_slice::<slidecraft_ui_egui::UiState>(&bytes)
|
||||
&& let Ok(ui) = serde_json::from_slice::<deckcraft_ui_egui::UiState>(&bytes)
|
||||
{
|
||||
app.ui = ui;
|
||||
}
|
||||
if let Some(p) = prefs_path().map(|p| p.with_file_name("prefs.json"))
|
||||
&& let Ok(bytes) = std::fs::read(&p)
|
||||
&& let Ok(prefs) = serde_json::from_slice::<slidecraft_engine::Prefs>(&bytes)
|
||||
&& let Ok(prefs) = serde_json::from_slice::<deckcraft_engine::Prefs>(&bytes)
|
||||
{
|
||||
app.session.prefs = prefs;
|
||||
}
|
||||
}
|
||||
|
||||
fn save_prefs(app: &SlideApp) {
|
||||
if std::env::var_os("SLIDECRAFT_NO_PREFS").is_some() {
|
||||
if std::env::var_os("DECKCRAFT_NO_PREFS").is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(p) = prefs_path() {
|
||||
|
|
@ -91,8 +91,8 @@ fn services() -> Services {
|
|||
),
|
||||
"video" => d.add_filter("Video", &["mp4", "m4v", "mov", "webm", "mkv", "wmv"]),
|
||||
_ => d
|
||||
.add_filter("Presentations", &["slidecraft", "pptx", "potx", "ppsx"])
|
||||
.add_filter("SlideCraft", &["slidecraft"])
|
||||
.add_filter("Presentations", &["deckcraft", "pptx", "potx", "ppsx"])
|
||||
.add_filter("DeckCraft", &["deckcraft"])
|
||||
.add_filter("PowerPoint", &["pptx", "potx", "ppsx"])
|
||||
.add_filter("Outline", &["txt", "md"]),
|
||||
};
|
||||
|
|
@ -109,23 +109,23 @@ fn services() -> Services {
|
|||
rgba.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?;
|
||||
Some(out)
|
||||
})),
|
||||
// Media is mixed and clocked by the output device; `SLIDECRAFT_NO_AUDIO` plays silently.
|
||||
audio_out: std::env::var_os("SLIDECRAFT_NO_AUDIO")
|
||||
// Media is mixed and clocked by the output device; `DECKCRAFT_NO_AUDIO` plays silently.
|
||||
audio_out: std::env::var_os("DECKCRAFT_NO_AUDIO")
|
||||
.is_none()
|
||||
.then(|| Box::new(audio::CpalOut::default()) as Box<dyn slidecraft_media::AudioOut>),
|
||||
.then(|| Box::new(audio::CpalOut::default()) as Box<dyn deckcraft_media::AudioOut>),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
const APP_ID: &str = "ai.storyteller.slidecraft";
|
||||
const APP_ID: &str = "ai.storyteller.deckcraft";
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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 sample = false;
|
||||
let mut show = false;
|
||||
|
|
@ -136,7 +136,7 @@ fn main() -> eframe::Result {
|
|||
"--sample" => sample = true,
|
||||
"--show" => show = true,
|
||||
"--version" => {
|
||||
println!("slidecraft {}", env!("CARGO_PKG_VERSION"));
|
||||
println!("deckcraft {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
_ => files.push(a),
|
||||
|
|
@ -144,7 +144,7 @@ fn main() -> eframe::Result {
|
|||
}
|
||||
let mut options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("SlideCraft")
|
||||
.with_title("DeckCraft")
|
||||
.with_inner_size([1440.0, 900.0])
|
||||
.with_min_inner_size([900.0, 560.0])
|
||||
.with_drag_and_drop(true)
|
||||
|
|
@ -158,21 +158,21 @@ fn main() -> eframe::Result {
|
|||
options.viewport = options.viewport.with_icon(icon);
|
||||
}
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
if std::env::var_os("DISPLAY").is_some() && std::env::var_os("SLIDECRAFT_WAYLAND").is_none() {
|
||||
if std::env::var_os("DISPLAY").is_some() && std::env::var_os("DECKCRAFT_WAYLAND").is_none() {
|
||||
options.event_loop_builder = Some(Box::new(|b| {
|
||||
use winit::platform::x11::EventLoopBuilderExtX11;
|
||||
b.with_x11();
|
||||
}));
|
||||
}
|
||||
eframe::run_native(
|
||||
"SlideCraft",
|
||||
"DeckCraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
let mut app = SlideApp::new(Session::new(), services());
|
||||
load_prefs(&mut app);
|
||||
// AutoRecover: reopen what a previous run left unsaved, then keep it current.
|
||||
if std::env::var_os("SLIDECRAFT_NO_RECOVERY").is_none() {
|
||||
app.session.recovery_dir = slidecraft_engine::recovery::default_dir();
|
||||
if std::env::var_os("DECKCRAFT_NO_RECOVERY").is_none() {
|
||||
app.session.recovery_dir = deckcraft_engine::recovery::default_dir();
|
||||
if let Ok(v) = app.session.execute("file.recovery.open", &serde_json::json!({}))
|
||||
&& let Some(n) = v.get("opened").and_then(|o| o.as_array()).map(Vec::len).filter(|n| *n > 0)
|
||||
{
|
||||
|
|
@ -184,12 +184,12 @@ fn main() -> eframe::Result {
|
|||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
app = app.with_control(rx);
|
||||
}
|
||||
if sample && let Err(e) = slidecraft_engine::sample::open_sample(&mut app.session) {
|
||||
eprintln!("slidecraft: sample: {e}");
|
||||
if sample && let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) {
|
||||
eprintln!("deckcraft: sample: {e}");
|
||||
}
|
||||
for f in files {
|
||||
if let Err(e) = app.open_path(&f) {
|
||||
eprintln!("slidecraft: {f}: {e}");
|
||||
eprintln!("deckcraft: {f}: {e}");
|
||||
}
|
||||
}
|
||||
if show {
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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…</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 818 B After Width: | Height: | Size: 818 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
|
@ -1,14 +1,14 @@
|
|||
[package]
|
||||
name = "slidecraft-anim"
|
||||
name = "deckcraft-anim"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "SlideCraft slide show timing: animation sequences, per-shape animation state over time, and transition frame layouts"
|
||||
description = "DeckCraft slide show timing: animation sequences, per-shape animation state over time, and transition frame layouts"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
slidecraft-model = { workspace = true }
|
||||
slidecraft-geom = { workspace = true }
|
||||
deckcraft-model = { workspace = true }
|
||||
deckcraft-geom = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use slidecraft_model::{AnimClass, Rgba, Xfrm};
|
||||
use deckcraft_model::{AnimClass, Rgba, Xfrm};
|
||||
|
||||
use crate::AnimState;
|
||||
use crate::easing::{bounce_out, ease, smooth};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Slide show timing for SlideCraft.
|
||||
//! Slide show timing for DeckCraft.
|
||||
//!
|
||||
//! - [`Timeline`]: a slide's animation list grouped into click steps, with per-shape (and
|
||||
//! per-paragraph) [`AnimState`] at any point of the show.
|
||||
|
|
@ -22,8 +22,8 @@ mod transition;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use deckcraft_model::Rgba;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use slidecraft_model::Rgba;
|
||||
|
||||
pub use easing::ease;
|
||||
pub use morph::{morph_pairs, morph_xfrm};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Morph: matching shapes between two slides and interpolating their boxes.
|
||||
|
||||
use slidecraft_model::{Shape, ShapeId, Slide, Xfrm};
|
||||
use deckcraft_model::{Shape, ShapeId, Slide, Xfrm};
|
||||
|
||||
use crate::clampf;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Slide show navigation: click steps, slide order (hidden slides, custom shows, ranges), loop
|
||||
//! and automatic advance.
|
||||
|
||||
use slidecraft_model::Presentation;
|
||||
use deckcraft_model::Presentation;
|
||||
|
||||
use crate::Timeline;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use slidecraft_model::anim::{ANIMATIONS, TRANSITIONS, TextBuild};
|
||||
use slidecraft_model::{
|
||||
use deckcraft_model::anim::{ANIMATIONS, TRANSITIONS, TextBuild};
|
||||
use deckcraft_model::{
|
||||
AnimClass, AnimStart, Animation, ColorRef, CustomShow, Presentation, Rgba, Shape, ShapeId, Slide, SlideId, TextBody, Transition, Xfrm,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! A slide's animation sequence: click steps, effect start times and per-shape state over time.
|
||||
|
||||
use slidecraft_model::anim::TextBuild;
|
||||
use slidecraft_model::style::Fill;
|
||||
use slidecraft_model::{AnimClass, AnimStart, Animation, ColorScheme, Rgba, SchemeSlot, ShapeId, Slide, Xfrm};
|
||||
use deckcraft_model::anim::TextBuild;
|
||||
use deckcraft_model::style::Fill;
|
||||
use deckcraft_model::{AnimClass, AnimStart, Animation, ColorScheme, Rgba, SchemeSlot, ShapeId, Slide, Xfrm};
|
||||
|
||||
use crate::AnimState;
|
||||
use crate::effects::{EffectSpec, exit_base, motion_path};
|
||||
|
|
@ -116,7 +116,7 @@ fn sanitize_xfrm(x: Xfrm, sw: f64, sh: f64) -> Xfrm {
|
|||
}
|
||||
|
||||
fn default_scheme() -> Option<ColorScheme> {
|
||||
slidecraft_model::theme::builtin_color_schemes().into_iter().next()
|
||||
deckcraft_model::theme::builtin_color_schemes().into_iter().next()
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
|
|
@ -139,11 +139,11 @@ impl Timeline {
|
|||
owned.as_ref()
|
||||
}
|
||||
};
|
||||
let resolve = |c: &slidecraft_model::ColorRef| -> Rgba {
|
||||
let resolve = |c: &deckcraft_model::ColorRef| -> Rgba {
|
||||
match scheme {
|
||||
Some(s) => c.resolve(s, None),
|
||||
None => match &c.base {
|
||||
slidecraft_model::ColorBase::Rgb { rgb } => *rgb,
|
||||
deckcraft_model::ColorBase::Rgb { rgb } => *rgb,
|
||||
_ => Rgba::rgb(0x15, 0x60, 0x82),
|
||||
},
|
||||
}
|
||||
|
|
@ -180,7 +180,7 @@ impl Timeline {
|
|||
(None, TextBuild::ByParagraph) => {
|
||||
let mut v: Vec<Option<usize>> = vec![];
|
||||
if let Some(t) = shape.and_then(|s| s.text.as_ref()) {
|
||||
let filled = |p: &slidecraft_model::text::Paragraph| p.runs.iter().any(|r| !r.text.trim().is_empty());
|
||||
let filled = |p: &deckcraft_model::text::Paragraph| p.runs.iter().any(|r| !r.text.trim().is_empty());
|
||||
let top = t.paragraphs.iter().filter(|p| filled(p)).map(|p| p.level).min().unwrap_or(0);
|
||||
let mut owner: Option<usize> = None;
|
||||
for (i, p) in t.paragraphs.iter().enumerate() {
|
||||
|
|
@ -465,7 +465,7 @@ fn status(e: &EffectTiming, step: usize, t: f64) -> Status {
|
|||
Status::Active(p.clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
fn build_spec(a: &Animation, xf: Xfrm, sw: f64, sh: f64, resolve: &dyn Fn(&slidecraft_model::ColorRef) -> Rgba) -> EffectSpec {
|
||||
fn build_spec(a: &Animation, xf: Xfrm, sw: f64, sh: f64, resolve: &dyn Fn(&deckcraft_model::ColorRef) -> Rgba) -> EffectSpec {
|
||||
let effect = if a.class == AnimClass::Exit { exit_base(&a.effect).to_string() } else { a.effect.clone() };
|
||||
let path = if a.class == AnimClass::Path { Some(motion_path(&a.effect, &a.option, a.path.as_deref())) } else { None };
|
||||
EffectSpec {
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ fn scale_rect(r: R, s: f64) -> R {
|
|||
|
||||
// ---------- effects ----------
|
||||
|
||||
/// Layers of the transition `kind` (a [`slidecraft_model::anim::TRANSITIONS`] id) with `option` at
|
||||
/// Layers of the transition `kind` (a [`deckcraft_model::anim::TRANSITIONS`] id) with `option` at
|
||||
/// progress `t` (0..1). Draw in order over black. At `t >= 1` the result is the new slide alone;
|
||||
/// at `t <= 0` (or NaN) the old slide alone. Morph returns a plain crossfade (the UI renders morph
|
||||
/// frames with [`crate::morph_pairs`]).
|
||||
|
|
@ -736,7 +736,7 @@ fn layers(kind: &str, option: &str, t: f64, depth: u32) -> Vec<Layer> {
|
|||
"random" if depth == 0 => {
|
||||
let h = option.bytes().fold(0x51ED_u64, |h, b| hash64(h ^ b as u64));
|
||||
let k = RANDOM_KINDS.get((h % RANDOM_KINDS.len() as u64) as usize).copied().unwrap_or("fade");
|
||||
let opt = slidecraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == k).and_then(|x| x.4.first().copied()).unwrap_or("");
|
||||
let opt = deckcraft_model::anim::TRANSITIONS.iter().find(|x| x.0 == k).and_then(|x| x.4.first().copied()).unwrap_or("");
|
||||
layers(k, opt, t, depth + 1)
|
||||
}
|
||||
_ => crossfade(t),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "slidecraft-av1"
|
||||
name = "deckcraft-av1"
|
||||
description = "Clean-room AV1 decoder (AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
|
@ -14,6 +14,6 @@ default = ["threads"]
|
|||
threads = ["dep:rayon"]
|
||||
|
||||
[dependencies]
|
||||
slidecraft-bitstream = { workspace = true }
|
||||
deckcraft-bitstream = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rayon = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "slidecraft-bitstream"
|
||||
name = "deckcraft-bitstream"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Bit-level reading and writing shared by SlideCraft's codec crates (ported from FilmCraft).
|
||||
//! Bit-level reading and writing shared by DeckCraft's codec crates (ported from FilmCraft).
|
||||
//!
|
||||
//! - [`BitReader`]: MSB-first reader with Exp-Golomb (`ue(v)`, `se(v)`), as used by H.264/HEVC/AV1 headers.
|
||||
//! - [`BitWriter`]: the matching writer (for encoders and header rewriting).
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "slidecraft-color"
|
||||
name = "deckcraft-color"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "SlideCraft colour: RGB/HSL, theme colour slots, DrawingML-style colour transforms"
|
||||
description = "DeckCraft colour: RGB/HSL, theme colour slots, DrawingML-style colour transforms"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! SlideCraft colour: sRGB colours, theme colour slots and DrawingML-style colour transforms
|
||||
//! DeckCraft colour: sRGB colours, theme colour slots and DrawingML-style colour transforms
|
||||
//! (tint, shade, luminance modulation/offset, alpha…), so theme-relative colours resolve the way
|
||||
//! presentation files expect.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
[package]
|
||||
name = "slidecraft-engine"
|
||||
name = "deckcraft-engine"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "SlideCraft engine: sessions, undo history, selection, the command registry every frontend dispatches through, and pointer tools"
|
||||
description = "DeckCraft engine: sessions, undo history, selection, the command registry every frontend dispatches through, and pointer tools"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
slidecraft-geom = { workspace = true }
|
||||
slidecraft-color = { workspace = true }
|
||||
slidecraft-model = { workspace = true }
|
||||
slidecraft-fonts = { workspace = true }
|
||||
slidecraft-text = { workspace = true }
|
||||
slidecraft-render = { workspace = true }
|
||||
slidecraft-pdf = { workspace = true }
|
||||
slidecraft-format = { workspace = true }
|
||||
slidecraft-pptx = { workspace = true }
|
||||
slidecraft-media = { workspace = true }
|
||||
deckcraft-geom = { workspace = true }
|
||||
deckcraft-color = { workspace = true }
|
||||
deckcraft-model = { workspace = true }
|
||||
deckcraft-fonts = { workspace = true }
|
||||
deckcraft-text = { workspace = true }
|
||||
deckcraft-render = { workspace = true }
|
||||
deckcraft-pdf = { workspace = true }
|
||||
deckcraft-format = { workspace = true }
|
||||
deckcraft-pptx = { workspace = true }
|
||||
deckcraft-media = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
i_overlay = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::anim::{ANIMATIONS, AnimClass, AnimStart, Animation, TextBuild, animation_info};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::anim::{ANIMATIONS, AnimClass, AnimStart, Animation, TextBuild, animation_info};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session};
|
||||
|
|
@ -227,7 +227,7 @@ fn timing(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let delay = p.get("delay").and_then(Value::as_u64).map(|v| v.min(600_000) as u32);
|
||||
let repeat = p.get("repeat").and_then(Value::as_u64).map(|v| v.min(u32::MAX as u64) as u32);
|
||||
let rewind = bool_param(p, "rewind");
|
||||
let trigger = p.get("trigger").map(|v| v.as_u64().and_then(|x| u32::try_from(x).ok()).map(slidecraft_model::ShapeId));
|
||||
let trigger = p.get("trigger").map(|v| v.as_u64().and_then(|x| u32::try_from(x).ok()).map(deckcraft_model::ShapeId));
|
||||
each_target(s, p, |a| {
|
||||
if let Some(x) = start {
|
||||
a.start = x;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Arrange: order, group, align, distribute, rotate and flip.
|
||||
|
||||
use deckcraft_geom::{Point, Rect, Xfrm};
|
||||
use deckcraft_model::{Shape, ShapeId, ShapeKind};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_geom::{Point, Rect, Xfrm};
|
||||
use slidecraft_model::{Shape, ShapeId, ShapeKind};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session};
|
||||
|
|
@ -145,7 +145,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
pub(crate) fn child_to_parent(group: &Xfrm, ch: &Xfrm, c: &Xfrm) -> Xfrm {
|
||||
let sx = if ch.w.abs() > 1e-9 { group.w / ch.w } else { 1.0 };
|
||||
let sy = if ch.h.abs() > 1e-9 { group.h / ch.h } else { 1.0 };
|
||||
let a = slidecraft_geom::group_child_affine(group, Rect::new(ch.x, ch.y, ch.x + ch.w, ch.y + ch.h));
|
||||
let a = deckcraft_geom::group_child_affine(group, Rect::new(ch.x, ch.y, ch.x + ch.w, ch.y + ch.h));
|
||||
let center = a * Point::new(c.x + c.w / 2.0, c.y + c.h / 2.0);
|
||||
let (w, h) = (c.w * sx.abs(), c.h * sy.abs());
|
||||
let mut rot = c.rot + group.rot;
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_geom::Size;
|
||||
use deckcraft_model::text::{Run, RunKind, TextBody};
|
||||
use deckcraft_model::theme::{builtin_color_schemes, builtin_font_schemes, builtin_themes};
|
||||
use deckcraft_model::{Background, Fill, PhType, Presentation, Shape, ShapeId, defaults};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_geom::Size;
|
||||
use slidecraft_model::text::{Run, RunKind, TextBody};
|
||||
use slidecraft_model::theme::{builtin_color_schemes, builtin_font_schemes, builtin_themes};
|
||||
use slidecraft_model::{Background, Fill, PhType, Presentation, Shape, ShapeId, defaults};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session, Target};
|
||||
|
|
@ -114,7 +114,7 @@ fn colors(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
if let Some(c) = &custom {
|
||||
for (k, v) in c {
|
||||
if let (Some(slot), Some(rgb)) = (slidecraft_color::SchemeSlot::from_xml(k), v.as_str().and_then(slidecraft_color::Rgba::from_hex)) {
|
||||
if let (Some(slot), Some(rgb)) = (deckcraft_color::SchemeSlot::from_xml(k), v.as_str().and_then(deckcraft_color::Rgba::from_hex)) {
|
||||
m.theme.colors.set(slot, rgb);
|
||||
}
|
||||
}
|
||||
|
|
@ -134,10 +134,10 @@ fn fonts(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
} else {
|
||||
let major = str_param(p, "major").ok_or_else(|| bad("design.fonts", "give `name`, or `major` and `minor`"))?;
|
||||
let minor = str_param(p, "minor").unwrap_or(major);
|
||||
slidecraft_model::theme::FontScheme {
|
||||
deckcraft_model::theme::FontScheme {
|
||||
name: "Custom".into(),
|
||||
major: slidecraft_model::theme::FontSet { latin: major.into(), ..Default::default() },
|
||||
minor: slidecraft_model::theme::FontSet { latin: minor.into(), ..Default::default() },
|
||||
major: deckcraft_model::theme::FontSet { latin: major.into(), ..Default::default() },
|
||||
minor: deckcraft_model::theme::FontSet { latin: minor.into(), ..Default::default() },
|
||||
}
|
||||
};
|
||||
s.edit(|doc, sel| {
|
||||
|
|
@ -155,16 +155,12 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Some(Background::Fill { fill: Fill::solid(c) })
|
||||
} else if let Some(i) = usize_param(p, "style") {
|
||||
// Background styles: 3 theme bg fills × 4 colours (bg1, bg2, tx1, tx2).
|
||||
let slots = [
|
||||
slidecraft_color::SchemeSlot::Bg1,
|
||||
slidecraft_color::SchemeSlot::Bg2,
|
||||
slidecraft_color::SchemeSlot::Tx2,
|
||||
slidecraft_color::SchemeSlot::Tx1,
|
||||
];
|
||||
let slots =
|
||||
[deckcraft_color::SchemeSlot::Bg1, deckcraft_color::SchemeSlot::Bg2, deckcraft_color::SchemeSlot::Tx2, deckcraft_color::SchemeSlot::Tx1];
|
||||
let i = i.clamp(1, 12) - 1;
|
||||
Some(Background::Ref {
|
||||
idx: 1001 + (i / 4) as u32,
|
||||
color: slidecraft_model::ColorRef::scheme(slots.get(i % 4).copied().unwrap_or(slidecraft_color::SchemeSlot::Bg1)),
|
||||
color: deckcraft_model::ColorRef::scheme(slots.get(i % 4).copied().unwrap_or(deckcraft_color::SchemeSlot::Bg1)),
|
||||
})
|
||||
} else if let Some(g) = p.get("gradient") {
|
||||
let mut g = super::shape::gradient_param(g).ok_or_else(|| bad("design.background", "gradient needs two or more `stops`"))?;
|
||||
|
|
@ -172,17 +168,17 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Some(Background::Fill { fill: Fill::Gradient(g) })
|
||||
} else if let Some(pt) = p.get("pattern") {
|
||||
Some(Background::Fill {
|
||||
fill: Fill::Pattern(slidecraft_model::style::PatternFill {
|
||||
fill: Fill::Pattern(deckcraft_model::style::PatternFill {
|
||||
preset: pt.get("preset").and_then(Value::as_str).unwrap_or("pct50").to_string(),
|
||||
fg: pt.get("fg").and_then(color_value).unwrap_or(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Accent1)),
|
||||
bg: pt.get("bg").and_then(color_value).unwrap_or(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Bg1)),
|
||||
fg: pt.get("fg").and_then(color_value).unwrap_or(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Accent1)),
|
||||
bg: pt.get("bg").and_then(color_value).unwrap_or(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1)),
|
||||
}),
|
||||
})
|
||||
} else if p.get("picture").is_some() {
|
||||
let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "design.background")?;
|
||||
let ct = super::insert::content_type(&name, &bytes);
|
||||
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
|
||||
Some(Background::Fill { fill: Fill::Picture(slidecraft_model::style::PictureFill { media, ..Default::default() }) })
|
||||
Some(Background::Fill { fill: Fill::Picture(deckcraft_model::style::PictureFill { media, ..Default::default() }) })
|
||||
} else {
|
||||
return Err(bad("design.background", "give `color`, `gradient`, `pattern`, `picture`, `style` or `reset`"));
|
||||
};
|
||||
|
|
@ -234,7 +230,7 @@ fn scale_shapes(shapes: &mut [Shape], sx: f64, sy: f64, k: f64, ox: f64, oy: f64
|
|||
x.h *= sy;
|
||||
}
|
||||
if let Some(t) = sh.text.as_mut() {
|
||||
slidecraft_model::edit::format_all(t, &|r| {
|
||||
deckcraft_model::edit::format_all(t, &|r| {
|
||||
if let Some(sz) = r.size.as_mut() {
|
||||
*sz = (*sz * k).max(1.0);
|
||||
}
|
||||
|
|
@ -316,7 +312,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
for i in list {
|
||||
let Some(layout) = doc.slides.get(i).map(|x| x.layout) else { continue };
|
||||
let lay = doc.layout(layout).map(|(_, l)| l.clone());
|
||||
let is_title = lay.as_ref().is_some_and(|l| l.kind == slidecraft_model::LayoutType::Title);
|
||||
let is_title = lay.as_ref().is_some_and(|l| l.kind == deckcraft_model::LayoutType::Title);
|
||||
let want = |k: PhType| {
|
||||
!(hf.hide_on_title && is_title)
|
||||
&& match k {
|
||||
|
|
@ -343,7 +339,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let Some(lp) = lp else { continue };
|
||||
let body = match k {
|
||||
PhType::SlideNum => TextBody {
|
||||
paragraphs: vec![slidecraft_model::Paragraph {
|
||||
paragraphs: vec![deckcraft_model::Paragraph {
|
||||
runs: vec![Run {
|
||||
text: "‹#›".into(),
|
||||
props: Default::default(),
|
||||
|
|
@ -354,7 +350,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
..Default::default()
|
||||
},
|
||||
PhType::Date if hf.date_text.is_empty() => TextBody {
|
||||
paragraphs: vec![slidecraft_model::Paragraph {
|
||||
paragraphs: vec![deckcraft_model::Paragraph {
|
||||
runs: vec![Run { text: today(), props: Default::default(), kind: RunKind::Field { field: "datetime1".into() } }],
|
||||
..Default::default()
|
||||
}],
|
||||
|
|
@ -444,7 +440,7 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let name = str_param(p, "name").unwrap_or("Custom Layout").to_string();
|
||||
s.edit(|doc, sel| {
|
||||
let mi = master_index(doc, sel);
|
||||
let id = slidecraft_model::LayoutId(doc.alloc_id());
|
||||
let id = deckcraft_model::LayoutId(doc.alloc_id());
|
||||
let tid = ShapeId(doc.alloc_id());
|
||||
let size = doc.slide_size;
|
||||
let m = Arc::make_mut(doc.masters.get_mut(mi).ok_or_else(|| bad("master.insertLayout", "no master"))?);
|
||||
|
|
@ -460,10 +456,10 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
};
|
||||
m.layouts.insert(
|
||||
at.min(m.layouts.len()),
|
||||
slidecraft_model::Layout {
|
||||
deckcraft_model::Layout {
|
||||
id,
|
||||
name,
|
||||
kind: slidecraft_model::LayoutType::Custom,
|
||||
kind: deckcraft_model::LayoutType::Custom,
|
||||
shapes: title.into_iter().collect(),
|
||||
background: None,
|
||||
show_master_shapes: true,
|
||||
|
|
@ -528,11 +524,11 @@ fn insert_placeholder(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
return Err(bad("master.insertPlaceholder", "select a layout in Slide Master view"));
|
||||
}
|
||||
let size = s.doc()?.doc.slide_size;
|
||||
let rect = rect_param(p, "rect").unwrap_or(slidecraft_geom::Xfrm::new(size.width * 0.25, size.height * 0.3, size.width * 0.5, size.height * 0.4));
|
||||
let rect = rect_param(p, "rect").unwrap_or(deckcraft_geom::Xfrm::new(size.width * 0.25, size.height * 0.3, size.width * 0.5, size.height * 0.4));
|
||||
let idx = s.doc()?.shapes().iter().filter_map(|x| x.ph.as_ref().map(|p| p.idx)).max().unwrap_or(0) + 1;
|
||||
let shape = Shape {
|
||||
xfrm: Some(rect),
|
||||
ph: Some(slidecraft_model::Placeholder { kind, idx, ..Default::default() }),
|
||||
ph: Some(deckcraft_model::Placeholder { kind, idx, ..Default::default() }),
|
||||
text: Some(TextBody::default()),
|
||||
..Default::default()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::{MediaId, Shape, ShapeId, Slide, SlideId};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::{MediaId, Shape, ShapeId, Slide, SlideId};
|
||||
|
||||
use super::*;
|
||||
use crate::{HistoryEntry, Result, Session};
|
||||
|
|
@ -49,10 +49,10 @@ fn slides_scope(s: &Session, p: &Value) -> bool {
|
|||
}
|
||||
|
||||
fn media_of_shapes(shapes: &[Shape], out: &mut Vec<MediaId>) {
|
||||
slidecraft_model::walk(shapes, &mut |s, _| {
|
||||
deckcraft_model::walk(shapes, &mut |s, _| {
|
||||
match &s.kind {
|
||||
slidecraft_model::ShapeKind::Picture { fill } => out.push(fill.media),
|
||||
slidecraft_model::ShapeKind::Media(m) => {
|
||||
deckcraft_model::ShapeKind::Picture { fill } => out.push(fill.media),
|
||||
deckcraft_model::ShapeKind::Media(m) => {
|
||||
out.push(m.media);
|
||||
if let Some(p) = m.poster {
|
||||
out.push(p);
|
||||
|
|
@ -60,7 +60,7 @@ fn media_of_shapes(shapes: &[Shape], out: &mut Vec<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);
|
||||
}
|
||||
});
|
||||
|
|
@ -73,8 +73,8 @@ fn copy_inner(s: &mut Session, p: &Value) -> Result<String> {
|
|||
if t.is_range() {
|
||||
let body = super::text::body_of(st, t).cloned().unwrap_or_default();
|
||||
let (a, b) = t.ordered();
|
||||
let plain = slidecraft_model::edit::text_range(&body, a, b);
|
||||
let sliced = slidecraft_model::edit::slice(&body, a, b);
|
||||
let plain = deckcraft_model::edit::text_range(&body, a, b);
|
||||
let sliced = deckcraft_model::edit::slice(&body, a, b);
|
||||
s.clipboard = crate::Clipboard { text: Some(sliced), plain: plain.clone(), ..Default::default() };
|
||||
return Ok(plain);
|
||||
}
|
||||
|
|
@ -126,8 +126,8 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
|
||||
/// Give pasted shapes fresh ids (groups included) and map connector ends.
|
||||
pub(crate) fn reid(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape]) {
|
||||
fn rec(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape], depth: usize) {
|
||||
pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) {
|
||||
fn rec(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape], depth: usize) {
|
||||
if depth > 64 {
|
||||
return;
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ pub(crate) fn reid(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape
|
|||
rec(doc, shapes, 0);
|
||||
}
|
||||
|
||||
fn bring_media(doc: &mut slidecraft_model::Presentation, media: &[slidecraft_model::MediaItem]) {
|
||||
fn bring_media(doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) {
|
||||
for m in media {
|
||||
if doc.media(m.id).is_none_or(|x| x.data != m.data) {
|
||||
if doc.media(m.id).is_none() {
|
||||
|
|
@ -319,7 +319,7 @@ pub(crate) fn remove_shapes(shapes: &mut Vec<Shape>, ids: &[ShapeId]) -> usize {
|
|||
|
||||
fn count(v: &[Shape]) -> usize {
|
||||
let mut n = 0;
|
||||
slidecraft_model::walk(v, &mut |_, _| n += 1);
|
||||
deckcraft_model::walk(v, &mut |_, _| n += 1);
|
||||
n
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! File menu: new, open, save, export, close, properties.
|
||||
|
||||
use deckcraft_model::{Presentation, defaults};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::{Presentation, defaults};
|
||||
|
||||
use super::*;
|
||||
use crate::{DocState, EngineError, Result, Session};
|
||||
|
|
@ -11,11 +11,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
cmd!(noundo "file.new", "New Presentation", ["File"], Some("Cmd+N"), "{theme?: name, size?: [w,h] pt, blank?: bool}", always, new),
|
||||
cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"), "{path}", always, open),
|
||||
cmd!(noundo "file.openBytes", "Open Data", [], None, "{name, data: base64}", always, open_bytes),
|
||||
cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?, format?: slidecraft|pptx}", has_doc, save),
|
||||
cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?, format?: deckcraft|pptx}", has_doc, save),
|
||||
cmd!(noundo "file.saveAs", "Save As…", ["File"], Some("Cmd+Shift+S"), "{path, format?}", has_doc, save_as),
|
||||
cmd!(noundo "file.saveTemplate", "Save as Template…", ["File"], None, "{path}", has_doc, save_as),
|
||||
cmd!(query "file.saveBytes", "Save to Data", [], None, "{format?: slidecraft|pptx} → {data: base64}", has_doc, save_bytes_cmd),
|
||||
cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format: png|jpeg|pptx|slidecraft|outline|pdf, slide?: index, all?: bool, scale?: px per pt; pdf: layout?: slides|notes|handouts, perPage?: 1|2|3|4|6|9, dpi?, slides?: [index], includeHidden?, textLayer?, frame?}", has_doc, export),
|
||||
cmd!(query "file.saveBytes", "Save to Data", [], None, "{format?: deckcraft|pptx} → {data: base64}", has_doc, save_bytes_cmd),
|
||||
cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format: png|jpeg|pptx|deckcraft|outline|pdf, slide?: index, all?: bool, scale?: px per pt; pdf: layout?: slides|notes|handouts, perPage?: 1|2|3|4|6|9, dpi?, slides?: [index], includeHidden?, textLayer?, frame?}", has_doc, export),
|
||||
cmd!(query "file.render", "Render Slide", [], None, "{slide?: index, scale?, edit?: bool} → {png: base64, width, height}", has_doc, render),
|
||||
cmd!(noundo "file.close", "Close", ["File"], Some("Cmd+W"), "{}", has_doc, close),
|
||||
cmd!(
|
||||
|
|
@ -36,8 +36,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
]
|
||||
}
|
||||
|
||||
fn theme_named(name: &str) -> Option<slidecraft_model::Theme> {
|
||||
slidecraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name))
|
||||
fn theme_named(name: &str) -> Option<deckcraft_model::Theme> {
|
||||
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> {
|
||||
|
|
@ -45,7 +45,7 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let size = p
|
||||
.get("size")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|a| Some(slidecraft_geom::Size::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?)))
|
||||
.and_then(|a| Some(deckcraft_geom::Size::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?)))
|
||||
.filter(|s| s.width >= 1.0 && s.height >= 1.0 && s.width < 5000.0 && s.height < 5000.0)
|
||||
.unwrap_or(defaults::WIDE);
|
||||
let doc = defaults::blank_presentation(size, theme, !bool_or(p, "blank", false));
|
||||
|
|
@ -56,25 +56,25 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
/// Recognise and read presentation bytes (native, PPTX).
|
||||
pub fn open_presentation(name: &str, bytes: &[u8]) -> Result<Presentation> {
|
||||
if slidecraft_format::sniff(bytes) {
|
||||
return slidecraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string()));
|
||||
if deckcraft_format::sniff(bytes) {
|
||||
return deckcraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string()));
|
||||
}
|
||||
if slidecraft_pptx::sniff(bytes) {
|
||||
let mut p = slidecraft_pptx::import(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
slidecraft_format::repair(&mut p);
|
||||
if deckcraft_pptx::sniff(bytes) {
|
||||
let mut p = deckcraft_pptx::import(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
deckcraft_format::repair(&mut p);
|
||||
return Ok(p);
|
||||
}
|
||||
let lower = name.to_ascii_lowercase();
|
||||
if lower.ends_with(".txt") || lower.ends_with(".md") {
|
||||
let text = String::from_utf8_lossy(bytes);
|
||||
let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false);
|
||||
slidecraft_format::outline_to_slides(&mut p, &text);
|
||||
deckcraft_format::outline_to_slides(&mut p, &text);
|
||||
if p.slides.is_empty() {
|
||||
p = defaults::new_presentation(None);
|
||||
}
|
||||
return Ok(p);
|
||||
}
|
||||
Err(EngineError::Other(format!("{name}: not a presentation SlideCraft can read")))
|
||||
Err(EngineError::Other(format!("{name}: not a presentation DeckCraft can read")))
|
||||
}
|
||||
|
||||
pub fn add_opened(s: &mut Session, name: &str, path: Option<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 bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let doc = open_presentation(path, &bytes)?;
|
||||
let native = slidecraft_format::sniff(&bytes) || slidecraft_pptx::sniff(&bytes);
|
||||
let native = deckcraft_format::sniff(&bytes) || deckcraft_pptx::sniff(&bytes);
|
||||
let i = add_opened(s, path, native.then(|| path.to_string()), doc);
|
||||
Ok(json!({"document": i, "slides": s.doc()?.doc.slides.len()}))
|
||||
}
|
||||
|
|
@ -104,24 +104,24 @@ fn open_bytes(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
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>> {
|
||||
match format {
|
||||
"pptx" | "potx" | "ppsx" => slidecraft_pptx::export(doc).map_err(|e| EngineError::Other(e.to_string())),
|
||||
"outline" | "txt" => Ok(slidecraft_format::slides_to_outline(doc).into_bytes()),
|
||||
"pdf" => slidecraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())),
|
||||
_ => slidecraft_format::save(doc).map_err(|e| EngineError::Other(e.to_string())),
|
||||
"pptx" | "potx" | "ppsx" => deckcraft_pptx::export(doc).map_err(|e| EngineError::Other(e.to_string())),
|
||||
"outline" | "txt" => Ok(deckcraft_format::slides_to_outline(doc).into_bytes()),
|
||||
"pdf" => deckcraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())),
|
||||
_ => deckcraft_format::save(doc).map_err(|e| EngineError::Other(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// PDF options from command params: `{layout: slides|notes|handouts, perPage, dpi, slides: [i],
|
||||
/// includeHidden, textLayer, frame}`.
|
||||
pub fn pdf_options(p: &Value) -> slidecraft_pdf::PdfOptions {
|
||||
let mut o = slidecraft_pdf::PdfOptions { layout: slidecraft_pdf::PageLayout::Slides, ..Default::default() };
|
||||
pub fn pdf_options(p: &Value) -> deckcraft_pdf::PdfOptions {
|
||||
let mut o = deckcraft_pdf::PdfOptions { layout: deckcraft_pdf::PageLayout::Slides, ..Default::default() };
|
||||
o.layout = match str_param(p, "layout").unwrap_or("slides") {
|
||||
"notes" => slidecraft_pdf::PageLayout::Notes,
|
||||
"handouts" => slidecraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).min(9) as u8 },
|
||||
_ => slidecraft_pdf::PageLayout::Slides,
|
||||
"notes" => deckcraft_pdf::PageLayout::Notes,
|
||||
"handouts" => deckcraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).min(9) as u8 },
|
||||
_ => deckcraft_pdf::PageLayout::Slides,
|
||||
};
|
||||
o.dpi = f64_or(p, "dpi", o.dpi).clamp(36.0, 600.0);
|
||||
o.slides = p.get("slides").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_u64().map(|x| x as usize)).collect());
|
||||
|
|
@ -133,7 +133,7 @@ pub fn pdf_options(p: &Value) -> slidecraft_pdf::PdfOptions {
|
|||
|
||||
/// PDF bytes for `doc` with [`pdf_options`] from `p`.
|
||||
pub fn pdf_bytes(doc: &Presentation, p: &Value) -> Result<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 {
|
||||
|
|
@ -149,7 +149,7 @@ pub fn format_for_path(path: &str) -> &'static str {
|
|||
} else if l.ends_with(".txt") || l.ends_with(".md") {
|
||||
"outline"
|
||||
} else {
|
||||
"slidecraft"
|
||||
"deckcraft"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ fn save_as(s: &mut Session, p: &Value) -> Result<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)?;
|
||||
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.
|
||||
pub fn render_png(doc: &Presentation, index: usize, scale: f64, edit: bool) -> (Vec<u8>, u32, u32) {
|
||||
let img =
|
||||
slidecraft_render::render_slide(doc, index, &slidecraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), edit, ..Default::default() });
|
||||
deckcraft_render::render_slide(doc, index, &deckcraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), edit, ..Default::default() });
|
||||
(img.to_png(), img.width, img.height)
|
||||
}
|
||||
|
||||
|
|
@ -240,10 +240,10 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if *i >= st.doc.slides.len() {
|
||||
return Err(bad("file.export", format!("no slide {i}")));
|
||||
}
|
||||
let img = slidecraft_render::render_slide(
|
||||
let img = deckcraft_render::render_slide(
|
||||
&st.doc,
|
||||
*i,
|
||||
&slidecraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), ..Default::default() },
|
||||
&deckcraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), ..Default::default() },
|
||||
);
|
||||
let bytes = if format == "png" { img.to_png() } else { img.to_jpeg(92) };
|
||||
let out = if slides.len() == 1 {
|
||||
|
|
@ -260,7 +260,7 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({"files": written}))
|
||||
}
|
||||
"pdf" => {
|
||||
let bytes = slidecraft_pdf::export(&st.doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let bytes = deckcraft_pdf::export(&st.doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
write_file(&path, &bytes)?;
|
||||
Ok(json!({"path": path, "bytes": bytes.len()}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
//! Home ▸ Font and Paragraph, Format menu: character, paragraph and text box formatting, and
|
||||
//! the Format Painter.
|
||||
|
||||
use deckcraft_model::edit as ed;
|
||||
use deckcraft_model::resolve;
|
||||
use deckcraft_model::text::{Align, Anchor, AutoFit, Bullet, Caps, ParaProps, RunProps, Spacing, Strike, TextDir};
|
||||
use deckcraft_model::{Effects, Fill, Line, Shape};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::edit as ed;
|
||||
use slidecraft_model::resolve;
|
||||
use slidecraft_model::text::{Align, Anchor, AutoFit, Bullet, Caps, ParaProps, RunProps, Spacing, Strike, TextDir};
|
||||
use slidecraft_model::{Effects, Fill, Line, Shape};
|
||||
|
||||
use super::text::{format_body, format_para, format_run};
|
||||
use super::*;
|
||||
|
|
@ -289,8 +289,8 @@ fn clear(s: &mut Session, _p: &Value) -> Result<Value> {
|
|||
}
|
||||
fn text_shadow(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let v = on_param(p, Some(current_run(s).shadow.is_some()));
|
||||
let sh = slidecraft_model::style::Shadow {
|
||||
color: slidecraft_model::ColorRef::rgb(slidecraft_color::Rgba::BLACK).with(slidecraft_color::ColorTransform::Alpha(43000)),
|
||||
let sh = deckcraft_model::style::Shadow {
|
||||
color: deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::BLACK).with(deckcraft_color::ColorTransform::Alpha(43000)),
|
||||
blur: 3.0,
|
||||
dist: 2.0,
|
||||
dir: 45.0,
|
||||
|
|
@ -328,7 +328,7 @@ fn case(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let mut pos = 0;
|
||||
for r in &mut para.runs {
|
||||
let len = r.char_len();
|
||||
if r.kind == slidecraft_model::text::RunKind::Text {
|
||||
if r.kind == deckcraft_model::text::RunKind::Text {
|
||||
let rs = from.max(pos).saturating_sub(pos);
|
||||
let re = to.min(pos + len).saturating_sub(pos);
|
||||
if re > rs {
|
||||
|
|
@ -349,7 +349,7 @@ fn case(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if let Some(t) = sh.text.as_mut() {
|
||||
for para in &mut t.paragraphs {
|
||||
for r in &mut para.runs {
|
||||
if r.kind == slidecraft_model::text::RunKind::Text {
|
||||
if r.kind == deckcraft_model::text::RunKind::Text {
|
||||
r.text = ed::change_case(&r.text, &mode);
|
||||
}
|
||||
}
|
||||
|
|
@ -541,7 +541,7 @@ pub struct Painted {
|
|||
pub fill: Option<Fill>,
|
||||
pub line: Option<Line>,
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
//! Insert tab: shapes, text boxes, pictures, tables, charts, audio, video, WordArt, fields.
|
||||
|
||||
use deckcraft_color::SchemeSlot;
|
||||
use deckcraft_geom::Xfrm;
|
||||
use deckcraft_model::chart::ChartType;
|
||||
use deckcraft_model::style::PictureFill;
|
||||
use deckcraft_model::text::{AutoFit, RunKind, TextBody};
|
||||
use deckcraft_model::{Chart, ColorRef, Fill, Geom, MediaClip, Shape, ShapeId, ShapeKind, ShapeStyle, Table};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_color::SchemeSlot;
|
||||
use slidecraft_geom::Xfrm;
|
||||
use slidecraft_model::chart::ChartType;
|
||||
use slidecraft_model::style::PictureFill;
|
||||
use slidecraft_model::text::{AutoFit, RunKind, TextBody};
|
||||
use slidecraft_model::{Chart, ColorRef, Fill, Geom, MediaClip, Shape, ShapeId, ShapeKind, ShapeStyle, Table};
|
||||
|
||||
use super::*;
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
|
@ -92,7 +92,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
|
||||
/// Default size for a click-inserted shape (1 inch square, PowerPoint style).
|
||||
fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm {
|
||||
let size = s.active().map(|d| d.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE);
|
||||
let size = s.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE);
|
||||
Xfrm::new((size.width - w) / 2.0, (size.height - h) / 2.0, w, h)
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Resu
|
|||
_ => shape
|
||||
.geom
|
||||
.preset_name()
|
||||
.and_then(slidecraft_geom::preset::info)
|
||||
.and_then(deckcraft_geom::preset::info)
|
||||
.map(|i| i.label.split(':').next().unwrap_or(i.label).trim().to_string())
|
||||
.unwrap_or_else(|| "Shape".into()),
|
||||
};
|
||||
|
|
@ -130,8 +130,8 @@ pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Resu
|
|||
|
||||
fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let preset = str_param(p, "preset").unwrap_or("rect");
|
||||
let info = slidecraft_geom::preset::info(preset).ok_or_else(|| bad("shape.insert", format!("unknown preset `{preset}`")))?;
|
||||
let line_like = slidecraft_geom::preset::is_line_like(preset);
|
||||
let info = deckcraft_geom::preset::info(preset).ok_or_else(|| bad("shape.insert", format!("unknown preset `{preset}`")))?;
|
||||
let line_like = deckcraft_geom::preset::is_line_like(preset);
|
||||
let rect = rect_param(p, "rect").unwrap_or_else(|| default_rect(s, 72.0, if line_like { 0.0 } else { 72.0 }));
|
||||
let adj: Vec<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 {
|
||||
|
|
@ -153,14 +153,14 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
if let Some(tb) = shape.text.as_mut() {
|
||||
for para in &mut tb.paragraphs {
|
||||
para.props.align = Some(slidecraft_model::text::Align::Center);
|
||||
para.props.align = Some(deckcraft_model::text::Align::Center);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
shape.kind = if preset.contains("Connector") { ShapeKind::Connector { start: None, end: None } } else { ShapeKind::Shape };
|
||||
if preset == "straightConnector1" {
|
||||
shape.line = Some(slidecraft_model::Line {
|
||||
tail: Some(slidecraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() }),
|
||||
shape.line = Some(deckcraft_model::Line {
|
||||
tail: Some(deckcraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() }),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
fn presets(_s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
Ok(Value::Array(
|
||||
slidecraft_geom::preset::CATALOG.iter().map(|i| json!({"name": i.name, "label": i.label, "category": i.category.label()})).collect(),
|
||||
deckcraft_geom::preset::CATALOG.iter().map(|i| json!({"name": i.name, "label": i.label, "category": i.category.label()})).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +287,7 @@ fn fitted(s: &Session, bytes: &[u8]) -> (f64, f64) {
|
|||
.and_then(|r| r.into_dimensions().ok())
|
||||
.map(|(w, h)| (w as f64 * 0.75, h as f64 * 0.75))
|
||||
.unwrap_or((288.0, 216.0));
|
||||
let size = s.active().map(|d| d.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE);
|
||||
let size = s.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE);
|
||||
let k = (size.width * 0.9 / w.max(1.0)).min(size.height * 0.9 / h.max(1.0)).min(1.0);
|
||||
(w * k, h * k)
|
||||
}
|
||||
|
|
@ -298,7 +298,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if !ct.starts_with("image/") {
|
||||
return Err(bad("insert.picture", format!("{name} is not a picture")));
|
||||
}
|
||||
if ct != "image/svg+xml" && slidecraft_render::decode_image(&bytes).is_none() {
|
||||
if ct != "image/svg+xml" && deckcraft_render::decode_image(&bytes).is_none() {
|
||||
return Err(EngineError::Other(format!("{name}: the picture can't be read")));
|
||||
}
|
||||
let (w, h) = fitted(s, &bytes);
|
||||
|
|
@ -308,7 +308,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
.into_iter()
|
||||
.find(|x| {
|
||||
x.ph_type()
|
||||
.is_some_and(|k| matches!(k, slidecraft_model::PhType::Picture | slidecraft_model::PhType::Obj | slidecraft_model::PhType::Body))
|
||||
.is_some_and(|k| matches!(k, deckcraft_model::PhType::Picture | deckcraft_model::PhType::Obj | deckcraft_model::PhType::Body))
|
||||
&& x.text.as_ref().is_none_or(|t| t.is_empty())
|
||||
})
|
||||
.map(|x| (x.id, xfrm_of(&d.doc, &d.selection, x)))
|
||||
|
|
@ -349,7 +349,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
sh.kind = shape.kind.clone();
|
||||
sh.xfrm = Some(rect);
|
||||
sh.ph = phv.map(|mut p| {
|
||||
p.kind = slidecraft_model::PhType::Picture;
|
||||
p.kind = deckcraft_model::PhType::Picture;
|
||||
p
|
||||
});
|
||||
sh.text = None;
|
||||
|
|
@ -426,7 +426,7 @@ fn chart(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if let Some(series) = p.get("series").and_then(Value::as_array) {
|
||||
c.series = series
|
||||
.iter()
|
||||
.map(|sv| slidecraft_model::chart::Series {
|
||||
.map(|sv| deckcraft_model::chart::Series {
|
||||
name: sv.get("name").and_then(Value::as_str).unwrap_or("Series").to_string(),
|
||||
values: sv.get("values").and_then(Value::as_array).map(|a| a.iter().map(Value::as_f64).collect()).unwrap_or_default(),
|
||||
..Default::default()
|
||||
|
|
@ -447,7 +447,7 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result<Value> {
|
|||
let (name, bytes) = media_bytes(p, cmd)?;
|
||||
let ct = content_type(&name, &bytes);
|
||||
let bytes = std::sync::Arc::new(bytes);
|
||||
let info = slidecraft_media::probe(&bytes);
|
||||
let info = deckcraft_media::probe(&bytes);
|
||||
let has_video = info.as_ref().is_ok_and(|i| i.video.is_some());
|
||||
let has_audio = info.as_ref().is_ok_and(|i| i.audio.is_some());
|
||||
match &info {
|
||||
|
|
@ -521,7 +521,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let mut shape = new_text_box(Xfrm::new(size.width * 0.2, size.height * 0.4, size.width * 0.6, 80.0), text);
|
||||
if let Some(t) = shape.text.as_mut() {
|
||||
for para in &mut t.paragraphs {
|
||||
para.props.align = Some(slidecraft_model::text::Align::Center);
|
||||
para.props.align = Some(deckcraft_model::text::Align::Center);
|
||||
for r in &mut para.runs {
|
||||
r.props.size = Some(54.0);
|
||||
r.props.bold = Some(style % 2 == 1);
|
||||
|
|
@ -532,7 +532,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
_ => SchemeSlot::Accent4,
|
||||
})));
|
||||
if style % 3 == 2 {
|
||||
r.props.outline = Some(slidecraft_model::Line::solid(ColorRef::scheme(SchemeSlot::Bg1), 1.0));
|
||||
r.props.outline = Some(deckcraft_model::Line::solid(ColorRef::scheme(SchemeSlot::Bg1), 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -568,7 +568,7 @@ fn symbol(s: &mut Session, p: &Value) -> Result<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") {
|
||||
Action::Url { url: u.to_string() }
|
||||
} else if let Some(i) = usize_param(p, "slide") {
|
||||
|
|
@ -589,7 +589,7 @@ fn hyperlink(s: &mut Session, p: &Value) -> Result<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 (preset, action) = match kind {
|
||||
"back" | "previous" => ("actionButtonBackPrevious", Some(Action::PreviousSlide)),
|
||||
|
|
@ -644,9 +644,9 @@ fn chart_data(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
.get("categories")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| a.iter().map(|v| v.as_str().map(String::from).unwrap_or_else(|| v.to_string())).collect());
|
||||
let series: Option<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()
|
||||
.map(|sv| slidecraft_model::chart::Series {
|
||||
.map(|sv| deckcraft_model::chart::Series {
|
||||
name: sv.get("name").and_then(Value::as_str).unwrap_or("Series").to_string(),
|
||||
values: sv.get("values").and_then(Value::as_array).map(|v| v.iter().map(Value::as_f64).collect()).unwrap_or_default(),
|
||||
..Default::default()
|
||||
|
|
@ -773,7 +773,7 @@ fn smart_art(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let mut body = TextBody::from_text(items.get(i).map(String::as_str).unwrap_or(""));
|
||||
body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
|
||||
for para in &mut body.paragraphs {
|
||||
para.props.align = Some(slidecraft_model::text::Align::Center);
|
||||
para.props.align = Some(deckcraft_model::text::Align::Center);
|
||||
for r in &mut para.runs {
|
||||
r.props.size = Some(20.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Inspection for agents: the deck, slides and shapes as JSON (verify work without screenshots).
|
||||
|
||||
use deckcraft_model::resolve::{self, Ctx};
|
||||
use deckcraft_model::{Shape, ShapeKind};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::resolve::{self, Ctx};
|
||||
use slidecraft_model::{Shape, ShapeKind};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session};
|
||||
|
|
@ -51,8 +51,8 @@ pub fn shape_summary(ctx: Option<&Ctx>, sh: &Shape, depth: usize) -> Value {
|
|||
let fill = ctx.map(|c| {
|
||||
let (f, ph) = resolve::fill(c, sh);
|
||||
match f {
|
||||
Some(slidecraft_model::Fill::Solid { color }) => json!(format!("#{}", c.color(&color, ph).hex())),
|
||||
Some(slidecraft_model::Fill::None) | None => json!("none"),
|
||||
Some(deckcraft_model::Fill::Solid { color }) => json!(format!("#{}", c.color(&color, ph).hex())),
|
||||
Some(deckcraft_model::Fill::None) | None => json!("none"),
|
||||
Some(other) => json!(serde_json::to_value(&other).ok().and_then(|v| v.get("kind").cloned()).unwrap_or_default()),
|
||||
}
|
||||
});
|
||||
|
|
@ -116,7 +116,7 @@ fn slide(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
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 bg = ctx.as_ref().map(|c| match resolve::background(c, Some(sl)) {
|
||||
(slidecraft_model::Fill::Solid { color }, ph) => json!(format!("#{}", c.color(&color, ph).hex())),
|
||||
(deckcraft_model::Fill::Solid { color }, ph) => json!(format!("#{}", c.color(&color, ph).hex())),
|
||||
(f, _) => serde_json::to_value(&f).ok().and_then(|v| v.get("kind").cloned()).unwrap_or_default(),
|
||||
});
|
||||
Ok(json!({
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
//! requests the UI host carries out with its player ([`crate::UiRequest::Media`]); the host reports
|
||||
//! back through [`Session::media_status`], which `media.info` returns with the probe.
|
||||
|
||||
use deckcraft_model::{MediaClip, ShapeId, ShapeKind};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::{MediaClip, ShapeId, ShapeKind};
|
||||
|
||||
use super::*;
|
||||
use crate::{EngineError, MediaStatus, Result, Session, UiRequest};
|
||||
|
|
@ -83,7 +83,7 @@ fn seek(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
request(s, p, "media.seek", "seek", Some(ms))
|
||||
}
|
||||
|
||||
pub(crate) fn probe_json(info: &slidecraft_media::MediaInfo) -> Value {
|
||||
pub(crate) fn probe_json(info: &deckcraft_media::MediaInfo) -> Value {
|
||||
json!({
|
||||
"container": info.container,
|
||||
"durationMs": info.duration_ms,
|
||||
|
|
@ -97,7 +97,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let (id, m) = target(s, p, "media.info")?;
|
||||
let st = s.doc()?;
|
||||
let item = st.doc.media(m.media).ok_or_else(|| EngineError::Other("the media data is missing".into()))?;
|
||||
let probe = slidecraft_media::probe(&item.data).map(|i| probe_json(&i)).unwrap_or_else(|e| json!({"error": e.to_string()}));
|
||||
let probe = deckcraft_media::probe(&item.data).map(|i| probe_json(&i)).unwrap_or_else(|e| json!({"error": e.to_string()}));
|
||||
Ok(json!({
|
||||
"id": id,
|
||||
"name": item.name,
|
||||
|
|
@ -119,8 +119,8 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
|
||||
/// A poster frame PNG for video bytes at `ms`.
|
||||
pub(crate) fn poster_png(bytes: &slidecraft_media::Bytes, ms: u64) -> Option<Vec<u8>> {
|
||||
match slidecraft_media::poster_png(bytes, ms as f64 / 1000.0) {
|
||||
pub(crate) fn poster_png(bytes: &deckcraft_media::Bytes, ms: u64) -> Option<Vec<u8>> {
|
||||
match deckcraft_media::poster_png(bytes, ms as f64 / 1000.0) {
|
||||
Ok(png) => Some(png),
|
||||
Err(e) => {
|
||||
log::info!("no poster frame: {e}");
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
//! the outlines of the selected shapes. The result takes the formatting of the first selected
|
||||
//! shape, like PowerPoint.
|
||||
|
||||
use deckcraft_geom::{PathEl, Rect, Xfrm};
|
||||
use deckcraft_model::{CustomPath, Geom, Shape, ShapeId, ShapeKind};
|
||||
use i_overlay::core::fill_rule::FillRule;
|
||||
use i_overlay::core::overlay_rule::OverlayRule;
|
||||
use i_overlay::float::single::SingleFloatOverlay;
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_geom::{PathEl, Rect, Xfrm};
|
||||
use slidecraft_model::{CustomPath, Geom, Shape, ShapeId, ShapeKind};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session};
|
||||
|
|
@ -31,12 +31,12 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
}
|
||||
|
||||
/// The filled outline of `s` in slide points (curves flattened).
|
||||
fn area_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, s: &Shape) -> Area {
|
||||
fn area_of(doc: &deckcraft_model::Presentation, sel: &crate::Selection, s: &Shape) -> Area {
|
||||
let x = xfrm_of(doc, sel, s);
|
||||
let geo = slidecraft_render::shape_geometry(s, x.w, x.h);
|
||||
let geo = deckcraft_render::shape_geometry(s, x.w, x.h);
|
||||
let a = x.affine();
|
||||
let mut out: Area = vec![];
|
||||
for sp in geo.paths.iter().filter(|p| p.fill != slidecraft_geom::preset::FillMode::None) {
|
||||
for sp in geo.paths.iter().filter(|p| p.fill != deckcraft_geom::preset::FillMode::None) {
|
||||
let mut cur: Vec<[f64; 2]> = vec![];
|
||||
let flush = |cur: &mut Vec<[f64; 2]>, out: &mut Area| {
|
||||
if cur.len() > 2 {
|
||||
|
|
@ -45,7 +45,7 @@ fn area_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, s: &Sha
|
|||
cur.clear();
|
||||
}
|
||||
};
|
||||
slidecraft_geom::preset_flatten(&sp.path, &mut |el| match el {
|
||||
deckcraft_geom::preset_flatten(&sp.path, &mut |el| match el {
|
||||
PathEl::MoveTo(p) => {
|
||||
flush(&mut cur, &mut out);
|
||||
let q = a * p;
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ pub mod table;
|
|||
pub mod text;
|
||||
pub mod transition;
|
||||
|
||||
use deckcraft_geom::Xfrm;
|
||||
use deckcraft_model::ShapeId;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use slidecraft_geom::Xfrm;
|
||||
use slidecraft_model::ShapeId;
|
||||
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
|
|
@ -207,31 +207,31 @@ pub(crate) fn ok() -> Result<Value> {
|
|||
}
|
||||
|
||||
/// 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)?;
|
||||
color_value(v)
|
||||
}
|
||||
|
||||
pub(crate) fn color_value(v: &Value) -> Option<slidecraft_model::ColorRef> {
|
||||
use slidecraft_model::ColorRef;
|
||||
pub(crate) fn color_value(v: &Value) -> Option<deckcraft_model::ColorRef> {
|
||||
use deckcraft_model::ColorRef;
|
||||
match v {
|
||||
Value::String(s) => {
|
||||
if let Some(slot) = slidecraft_color::SchemeSlot::from_xml(s) {
|
||||
if let Some(slot) = deckcraft_color::SchemeSlot::from_xml(s) {
|
||||
return Some(ColorRef::scheme(slot));
|
||||
}
|
||||
if let Some(c) = slidecraft_color::Rgba::from_hex(s) {
|
||||
if let Some(c) = deckcraft_color::Rgba::from_hex(s) {
|
||||
return Some(ColorRef::rgb(c));
|
||||
}
|
||||
slidecraft_color::preset(s).map(ColorRef::rgb)
|
||||
deckcraft_color::preset(s).map(ColorRef::rgb)
|
||||
}
|
||||
Value::Object(o) => {
|
||||
let mut c = if let Some(s) = o.get("scheme").and_then(Value::as_str) {
|
||||
ColorRef::scheme(slidecraft_color::SchemeSlot::from_xml(s)?)
|
||||
ColorRef::scheme(deckcraft_color::SchemeSlot::from_xml(s)?)
|
||||
} else {
|
||||
ColorRef::rgb(slidecraft_color::Rgba::from_hex(o.get("rgb").and_then(Value::as_str)?)?)
|
||||
ColorRef::rgb(deckcraft_color::Rgba::from_hex(o.get("rgb").and_then(Value::as_str)?)?)
|
||||
};
|
||||
for (k, val) in o {
|
||||
if let Some(t) = slidecraft_color::ColorTransform::from_xml(k, val.as_i64().and_then(|x| i32::try_from(x).ok())) {
|
||||
if let Some(t) = deckcraft_color::ColorTransform::from_xml(k, val.as_i64().and_then(|x| i32::try_from(x).ok())) {
|
||||
c.mods.push(t);
|
||||
}
|
||||
}
|
||||
|
|
@ -242,20 +242,20 @@ pub(crate) fn color_value(v: &Value) -> Option<slidecraft_model::ColorRef> {
|
|||
}
|
||||
|
||||
/// 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}")))
|
||||
}
|
||||
|
||||
/// Effective box of a shape (placeholders inherit theirs).
|
||||
pub fn xfrm_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape: &slidecraft_model::Shape) -> Xfrm {
|
||||
pub fn xfrm_of(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm {
|
||||
if let Some(x) = shape.xfrm {
|
||||
return x;
|
||||
}
|
||||
ctx_xfrm(doc, sel, shape)
|
||||
}
|
||||
|
||||
fn ctx_xfrm(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape: &slidecraft_model::Shape) -> Xfrm {
|
||||
use slidecraft_model::resolve::{self, Ctx};
|
||||
fn ctx_xfrm(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm {
|
||||
use deckcraft_model::resolve::{self, Ctx};
|
||||
match sel.target {
|
||||
crate::Target::Slides => doc.slides.get(sel.slide).and_then(|s| Ctx::for_slide(doc, s)).map(|c| resolve::xfrm(&c, shape)),
|
||||
crate::Target::Master { master } => doc.masters.get(master).map(|m| resolve::xfrm(&Ctx::for_master(doc, m), shape)),
|
||||
|
|
@ -267,21 +267,21 @@ fn ctx_xfrm(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape:
|
|||
}
|
||||
|
||||
/// Run `f` on each target shape (copy-on-write), materialising inherited boxes first.
|
||||
pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut slidecraft_model::Shape) -> Result<()>) -> Result<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)?;
|
||||
if ids.is_empty() {
|
||||
return Err(bad(cmd, "no shapes selected"));
|
||||
}
|
||||
s.edit(|doc, sel| {
|
||||
let boxes: Vec<(ShapeId, Xfrm)> = {
|
||||
let st_shapes: Vec<slidecraft_model::Shape> =
|
||||
ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| slidecraft_model::find_shape(v, *id)).cloned()).collect();
|
||||
let st_shapes: Vec<deckcraft_model::Shape> =
|
||||
ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)).cloned()).collect();
|
||||
st_shapes.iter().map(|sh| (sh.id, xfrm_of(doc, sel, sh))).collect()
|
||||
};
|
||||
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
|
||||
let mut n = 0;
|
||||
for id in &ids {
|
||||
if let Some(sh) = slidecraft_model::find_shape_mut(shapes, *id) {
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, *id) {
|
||||
if sh.xfrm.is_none() {
|
||||
sh.xfrm = boxes.iter().find(|(i, _)| i == id).map(|(_, x)| *x);
|
||||
}
|
||||
|
|
@ -296,7 +296,7 @@ pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn current_shapes<'a>(doc: &'a slidecraft_model::Presentation, sel: &crate::Selection) -> Option<&'a Vec<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 {
|
||||
crate::Target::Slides => doc.slides.get(sel.slide).map(|s| &s.shapes),
|
||||
crate::Target::Master { master } => doc.masters.get(master).map(|m| &m.shapes),
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::Comment;
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::Comment;
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session, UiRequest};
|
||||
|
|
@ -130,34 +130,34 @@ fn accessibility(s: &mut Session, _p: &Value) -> Result<Value> {
|
|||
if sl.title().trim().is_empty() {
|
||||
out.push(json!({"slide": i, "issue": "Missing slide title"}));
|
||||
}
|
||||
slidecraft_model::walk(&sl.shapes, &mut |sh, _| {
|
||||
deckcraft_model::walk(&sl.shapes, &mut |sh, _| {
|
||||
let needs_alt = matches!(
|
||||
sh.kind,
|
||||
slidecraft_model::ShapeKind::Picture { .. }
|
||||
| slidecraft_model::ShapeKind::Chart(_)
|
||||
| slidecraft_model::ShapeKind::Media(_)
|
||||
| slidecraft_model::ShapeKind::Group { .. }
|
||||
deckcraft_model::ShapeKind::Picture { .. }
|
||||
| deckcraft_model::ShapeKind::Chart(_)
|
||||
| deckcraft_model::ShapeKind::Media(_)
|
||||
| deckcraft_model::ShapeKind::Group { .. }
|
||||
);
|
||||
if needs_alt && sh.descr.trim().is_empty() && !sh.decorative {
|
||||
out.push(json!({"slide": i, "shape": sh.id, "name": sh.name, "issue": "Missing alternative text"}));
|
||||
}
|
||||
});
|
||||
// Low-contrast text against the slide background.
|
||||
if let Some(ctx) = slidecraft_model::resolve::Ctx::for_slide(&st.doc, sl) {
|
||||
let bg = match slidecraft_model::resolve::background(&ctx, Some(sl)) {
|
||||
(slidecraft_model::Fill::Solid { color }, ph) => Some(ctx.color(&color, ph)),
|
||||
if let Some(ctx) = deckcraft_model::resolve::Ctx::for_slide(&st.doc, sl) {
|
||||
let bg = match deckcraft_model::resolve::background(&ctx, Some(sl)) {
|
||||
(deckcraft_model::Fill::Solid { color }, ph) => Some(ctx.color(&color, ph)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(bg) = bg {
|
||||
for sh in &sl.shapes {
|
||||
let Some(t) = &sh.text else { continue };
|
||||
let Some(para) = t.paragraphs.iter().find(|p| !p.is_empty()) else { continue };
|
||||
let rp = slidecraft_model::resolve::run(&ctx, sh, para, ¶.runs.first().map(|r| r.props.clone()).unwrap_or_default());
|
||||
let (fill, _) = slidecraft_model::resolve::fill(&ctx, sh);
|
||||
let rp = deckcraft_model::resolve::run(&ctx, sh, para, ¶.runs.first().map(|r| r.props.clone()).unwrap_or_default());
|
||||
let (fill, _) = deckcraft_model::resolve::fill(&ctx, sh);
|
||||
if fill.as_ref().is_some_and(|f| !f.is_none()) {
|
||||
continue;
|
||||
}
|
||||
let fg = slidecraft_model::resolve::text_color(&ctx, &rp);
|
||||
let fg = deckcraft_model::resolve::text_color(&ctx, &rp);
|
||||
if fg.contrast(bg) < 3.0 {
|
||||
out.push(json!({"slide": i, "shape": sh.id, "name": sh.name, "issue": format!("Hard-to-read text contrast ({:.1}:1)", fg.contrast(bg))}));
|
||||
}
|
||||
|
|
@ -201,7 +201,7 @@ fn spelling(s: &mut Session, _p: &Value) -> Result<Value> {
|
|||
let st = s.doc()?;
|
||||
let mut out = vec![];
|
||||
for (i, sl) in st.doc.slides.iter().enumerate() {
|
||||
slidecraft_model::walk(&sl.shapes, &mut |sh, _| {
|
||||
deckcraft_model::walk(&sl.shapes, &mut |sh, _| {
|
||||
if let Some(t) = &sh.text {
|
||||
for w in t.text().split(|c: char| !c.is_alphanumeric() && c != '\'') {
|
||||
if COMMON.contains(&w.to_lowercase().as_str()) {
|
||||
|
|
@ -258,7 +258,7 @@ fn custom_show(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
doc.custom_shows.retain(|c| c.name != name);
|
||||
if !delete {
|
||||
let slides = idx.iter().filter_map(|i| doc.slides.get(*i).map(|x| x.id)).collect();
|
||||
doc.custom_shows.push(slidecraft_model::CustomShow { name: name.clone(), slides });
|
||||
doc.custom_shows.push(deckcraft_model::CustomShow { name: name.clone(), slides });
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
//! Shape Format: position and size, rotation, flips, geometry, fill, outline, effects, styles,
|
||||
//! names and alt text.
|
||||
|
||||
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
|
||||
use deckcraft_model::style::{Dash, Glow, Gradient, GradientShape, GradientStop, LineEnd, PatternFill, PictureFill, Reflection, Shadow};
|
||||
use deckcraft_model::{ColorRef, Effects, Fill, Geom, Line, ShapeStyle};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_color::{ColorTransform, Rgba, SchemeSlot};
|
||||
use slidecraft_model::style::{Dash, Glow, Gradient, GradientShape, GradientStop, LineEnd, PatternFill, PictureFill, Reflection, Shadow};
|
||||
use slidecraft_model::{ColorRef, Effects, Fill, Geom, Line, ShapeStyle};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session};
|
||||
|
|
@ -195,7 +195,7 @@ fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let v = f64_param(p, "value").ok_or_else(|| bad("shape.adjust", "missing `value`"))?;
|
||||
edit_shapes(s, p, "shape.adjust", |sh| {
|
||||
if let Geom::Preset { name, adj } = &mut sh.geom {
|
||||
let defaults = slidecraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
|
||||
let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
|
||||
while adj.len() <= i {
|
||||
let k = adj.len();
|
||||
adj.push(defaults.get(k).copied().unwrap_or(0.0));
|
||||
|
|
@ -210,7 +210,7 @@ fn adjust(s: &mut Session, p: &Value) -> Result<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 info = slidecraft_geom::preset::info(name).ok_or_else(|| bad("shape.change", format!("unknown preset `{name}`")))?;
|
||||
let info = deckcraft_geom::preset::info(name).ok_or_else(|| bad("shape.change", format!("unknown preset `{name}`")))?;
|
||||
edit_shapes(s, p, "shape.change", |sh| {
|
||||
sh.geom = Geom::preset(info.name);
|
||||
Ok(())
|
||||
|
|
@ -287,7 +287,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
// Transparency on an inherited (style) fill needs the effective colour.
|
||||
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);
|
||||
targets(s, p)?
|
||||
.into_iter()
|
||||
|
|
@ -296,7 +296,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
(
|
||||
id,
|
||||
ctx.as_ref().and_then(|c| {
|
||||
let (f, ph) = slidecraft_model::resolve::fill(c, sh);
|
||||
let (f, ph) = deckcraft_model::resolve::fill(c, sh);
|
||||
f.map(|f| match f {
|
||||
Fill::Solid { color } => Fill::solid(ColorRef::rgb(c.color(&color, ph))),
|
||||
other => other,
|
||||
|
|
@ -338,14 +338,14 @@ fn line(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let head = str_param(p, "head").map(line_end);
|
||||
let tail = str_param(p, "tail").map(line_end);
|
||||
let cap = str_param(p, "cap").map(|c| match c {
|
||||
"round" => slidecraft_model::style::LineCap::Round,
|
||||
"square" => slidecraft_model::style::LineCap::Square,
|
||||
_ => slidecraft_model::style::LineCap::Flat,
|
||||
"round" => deckcraft_model::style::LineCap::Round,
|
||||
"square" => deckcraft_model::style::LineCap::Square,
|
||||
_ => deckcraft_model::style::LineCap::Flat,
|
||||
});
|
||||
let join = str_param(p, "join").map(|c| match c {
|
||||
"bevel" => slidecraft_model::style::LineJoin::Bevel,
|
||||
"miter" => slidecraft_model::style::LineJoin::Miter,
|
||||
_ => slidecraft_model::style::LineJoin::Round,
|
||||
"bevel" => deckcraft_model::style::LineJoin::Bevel,
|
||||
"miter" => deckcraft_model::style::LineJoin::Miter,
|
||||
_ => deckcraft_model::style::LineJoin::Round,
|
||||
});
|
||||
edit_shapes(s, p, "shape.line", |sh| {
|
||||
if reset {
|
||||
|
|
@ -536,7 +536,7 @@ fn quick_style(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
sh.line.clone_from(&l);
|
||||
sh.effects = None;
|
||||
if let Some(t) = sh.text.as_mut() {
|
||||
slidecraft_model::edit::format_all(t, &|r| r.fill = None);
|
||||
deckcraft_model::edit::format_all(t, &|r| r.fill = None);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
|
|
@ -596,7 +596,7 @@ fn set_default(s: &mut Session, _p: &Value) -> Result<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")];
|
||||
edit_shapes(s, p, "picture.crop", |sh| {
|
||||
if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
for (i, v) in c.iter().enumerate() {
|
||||
if let (Some(v), Some(slot)) = (v, fill.crop.get_mut(i)) {
|
||||
*slot = v.clamp(-10.0, 0.95);
|
||||
|
|
@ -609,7 +609,7 @@ fn crop(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
fn picture_adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
edit_shapes(s, p, "picture.adjust", |sh| {
|
||||
if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
let a = &mut fill.adjust;
|
||||
if bool_or(p, "reset", false) {
|
||||
*a = Default::default();
|
||||
|
|
@ -636,7 +636,7 @@ fn picture_adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
fn picture_reset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
edit_shapes(s, p, "picture.reset", |sh| {
|
||||
if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
fill.adjust = Default::default();
|
||||
fill.crop = [0.0; 4];
|
||||
fill.alpha = None;
|
||||
|
|
@ -652,7 +652,7 @@ fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let ct = super::insert::content_type(&name, &bytes);
|
||||
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
|
||||
edit_shapes(s, p, "picture.change", |sh| {
|
||||
if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
fill.media = media;
|
||||
fill.crop = [0.0; 4];
|
||||
}
|
||||
|
|
@ -662,7 +662,7 @@ fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
fn media_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
edit_shapes(s, p, "media.options", |sh| {
|
||||
if let slidecraft_model::ShapeKind::Media(m) = &mut sh.kind {
|
||||
if let deckcraft_model::ShapeKind::Media(m) = &mut sh.kind {
|
||||
if let Some(v) = bool_param(p, "autoplay") {
|
||||
m.autoplay = v;
|
||||
}
|
||||
|
|
@ -703,8 +703,8 @@ fn media_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({}))
|
||||
}
|
||||
|
||||
fn shape_id(p: &Value, key: &str) -> Option<slidecraft_model::ShapeId> {
|
||||
p.get(key).and_then(Value::as_u64).map(|v| slidecraft_model::ShapeId(v as u32))
|
||||
fn shape_id(p: &Value, key: &str) -> Option<deckcraft_model::ShapeId> {
|
||||
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> {
|
||||
|
|
@ -722,7 +722,7 @@ fn connect(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if a.is_empty() || b.is_empty() {
|
||||
return Err(bad("shape.connect", "both shapes need connection sites (lines and groups have none)"));
|
||||
}
|
||||
let pick = |v: &[slidecraft_geom::Point], key: &str| usize_param(p, key).filter(|i| *i < v.len());
|
||||
let pick = |v: &[deckcraft_geom::Point], key: &str| usize_param(p, key).filter(|i| *i < v.len());
|
||||
let (ia, ib) = match (pick(&a, "fromSite"), pick(&b, "toSite")) {
|
||||
(Some(i), Some(j)) => (i, j),
|
||||
(fi, fj) => {
|
||||
|
|
@ -744,24 +744,24 @@ fn connect(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
None => {
|
||||
let preset = str_param(p, "preset").unwrap_or("straightConnector1");
|
||||
let r = s.execute("shape.insert", &json!({"preset": preset, "rect": [p0.x, p0.y, 1.0, 1.0]}))?;
|
||||
slidecraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.connect", "insert failed"))? as u32)
|
||||
deckcraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.connect", "insert failed"))? as u32)
|
||||
}
|
||||
};
|
||||
s.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.connect", "no slide"))?;
|
||||
let sh = slidecraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.connect", "no such line"))?;
|
||||
let sh = deckcraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.connect", "no such line"))?;
|
||||
if !sh.is_line() {
|
||||
return Err(bad("shape.connect", "`id` is not a line or connector"));
|
||||
}
|
||||
sh.xfrm = Some(crate::tools::line_xfrm(p0, p1, 0.0));
|
||||
sh.kind = slidecraft_model::ShapeKind::Connector { start: Some((from, ia as u32)), end: Some((to, ib as u32)) };
|
||||
sh.kind = deckcraft_model::ShapeKind::Connector { start: Some((from, ia as u32)), end: Some((to, ib as u32)) };
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"id": id.0, "fromSite": ia, "toSite": ib}))
|
||||
}
|
||||
|
||||
/// Path data through `pts` (shape-local): straight segments, or a Catmull-Rom curve as cubics.
|
||||
pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool) -> String {
|
||||
pub fn freeform_path(pts: &[deckcraft_geom::Point], closed: bool, smooth: bool) -> String {
|
||||
let f = |v: f64| {
|
||||
let s = format!("{:.2}", v);
|
||||
s.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
|
|
@ -769,7 +769,7 @@ pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool)
|
|||
let mut d = format!("M {} {}", f(pts[0].x), f(pts[0].y));
|
||||
let n = pts.len();
|
||||
if smooth && n > 2 {
|
||||
let at = |i: isize| -> slidecraft_geom::Point {
|
||||
let at = |i: isize| -> deckcraft_geom::Point {
|
||||
if closed { pts[i.rem_euclid(n as isize) as usize] } else { pts[i.clamp(0, n as isize - 1) as usize] }
|
||||
};
|
||||
let segs = if closed { n } else { n - 1 };
|
||||
|
|
@ -791,12 +791,12 @@ pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool)
|
|||
}
|
||||
|
||||
fn freeform(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let pts: Vec<slidecraft_geom::Point> = p
|
||||
let pts: Vec<deckcraft_geom::Point> = p
|
||||
.get("points")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(|v| Some(slidecraft_geom::Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)))
|
||||
.filter_map(|v| Some(deckcraft_geom::Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)))
|
||||
.filter(|q| q.x.is_finite() && q.y.is_finite())
|
||||
.take(20_000)
|
||||
.collect()
|
||||
|
|
@ -807,30 +807,30 @@ fn freeform(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
let closed = bool_or(p, "closed", false) && pts.len() > 2;
|
||||
let smooth = bool_or(p, "smooth", false);
|
||||
let mut bb = slidecraft_geom::Rect::from_points(pts[0], pts[0]);
|
||||
let mut bb = deckcraft_geom::Rect::from_points(pts[0], pts[0]);
|
||||
for q in &pts {
|
||||
bb = bb.union_pt(*q);
|
||||
}
|
||||
let (w, h) = (bb.width().max(1.0), bb.height().max(1.0));
|
||||
let local: Vec<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);
|
||||
// Closed shapes take the default shape style, open paths the line style.
|
||||
let r = s.execute("shape.insert", &json!({"preset": if closed { "rect" } else { "line" }, "rect": [bb.x0, bb.y0, w, h]}))?;
|
||||
let id = slidecraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.freeform", "insert failed"))? as u32);
|
||||
let id = deckcraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.freeform", "insert failed"))? as u32);
|
||||
s.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.freeform", "no slide"))?;
|
||||
let sh = slidecraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.freeform", "insert failed"))?;
|
||||
let sh = deckcraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.freeform", "insert failed"))?;
|
||||
sh.geom = Geom::Custom {
|
||||
paths: vec![slidecraft_model::CustomPath {
|
||||
paths: vec![deckcraft_model::CustomPath {
|
||||
w,
|
||||
h,
|
||||
d: d.clone(),
|
||||
fill: if closed { slidecraft_geom::preset::FillMode::Norm } else { slidecraft_geom::preset::FillMode::None },
|
||||
fill: if closed { deckcraft_geom::preset::FillMode::Norm } else { deckcraft_geom::preset::FillMode::None },
|
||||
stroke: true,
|
||||
}],
|
||||
};
|
||||
sh.xfrm = Some(slidecraft_geom::Xfrm::new(bb.x0, bb.y0, w, h));
|
||||
sh.kind = slidecraft_model::ShapeKind::Shape;
|
||||
sh.xfrm = Some(deckcraft_geom::Xfrm::new(bb.x0, bb.y0, w, h));
|
||||
sh.kind = deckcraft_model::ShapeKind::Shape;
|
||||
sh.name = format!("Freeform {}", id.0);
|
||||
Ok(())
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::text::TextBody;
|
||||
use deckcraft_model::{LayoutId, LayoutType, Presentation, Section, SlideId, defaults};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::text::TextBody;
|
||||
use slidecraft_model::{LayoutId, LayoutType, Presentation, Section, SlideId, defaults};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session, Target};
|
||||
|
|
@ -173,12 +173,12 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
.get(*i)
|
||||
.map(|x| {
|
||||
let mut v = vec![];
|
||||
slidecraft_model::walk(&x.shapes, &mut |sh, _| v.push(sh.id));
|
||||
deckcraft_model::walk(&x.shapes, &mut |sh, _| v.push(sh.id));
|
||||
v
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut new_shape_ids = vec![];
|
||||
slidecraft_model::walk(©.shapes, &mut |sh, _| new_shape_ids.push(sh.id));
|
||||
deckcraft_model::walk(©.shapes, &mut |sh, _| new_shape_ids.push(sh.id));
|
||||
for a in &mut copy.animations {
|
||||
if let Some(pos) = old_ids.iter().position(|x| *x == a.shape) {
|
||||
a.shape = new_shape_ids.get(pos).copied().unwrap_or(a.shape);
|
||||
|
|
@ -360,16 +360,16 @@ fn hide(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
/// Re-map a slide's placeholders onto a new layout (content kept, matched by type/index).
|
||||
pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> Result<()> {
|
||||
let lay_shapes: Vec<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"))?;
|
||||
let mut fresh = vec![];
|
||||
for ls in lay_shapes.iter().filter(|x| x.ph.as_ref().is_some_and(|p| !p.kind.is_footer_kind())) {
|
||||
let id = slidecraft_model::ShapeId(doc.alloc_id());
|
||||
let id = deckcraft_model::ShapeId(doc.alloc_id());
|
||||
fresh.push((id, ls.clone()));
|
||||
}
|
||||
let slide = doc.slides.get_mut(index).ok_or_else(|| bad("slide.layout", "no slide"))?;
|
||||
let slide = Arc::make_mut(slide);
|
||||
let mut old: Vec<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![];
|
||||
for (id, ls) in fresh {
|
||||
let Some(lph) = ls.ph.clone() else { continue };
|
||||
|
|
@ -389,13 +389,13 @@ pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> R
|
|||
out.push(o);
|
||||
}
|
||||
None => {
|
||||
out.push(slidecraft_model::Shape { id, name: ls.name.clone(), ph: Some(lph), text: Some(TextBody::default()), ..Default::default() })
|
||||
out.push(deckcraft_model::Shape { id, name: ls.name.clone(), ph: Some(lph), text: Some(TextBody::default()), ..Default::default() })
|
||||
}
|
||||
}
|
||||
}
|
||||
// Leftover placeholders with content stay as they were (positioned); empty ones go.
|
||||
for o in old {
|
||||
let empty_ph = o.ph.is_some() && o.text.as_ref().is_none_or(|t| t.is_empty()) && matches!(o.kind, slidecraft_model::ShapeKind::Shape);
|
||||
let empty_ph = o.ph.is_some() && o.text.as_ref().is_none_or(|t| t.is_empty()) && matches!(o.kind, deckcraft_model::ShapeKind::Shape);
|
||||
if !empty_ph {
|
||||
out.push(o);
|
||||
}
|
||||
|
|
@ -574,7 +574,7 @@ fn from_outline(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let text = str_param(p, "text").unwrap_or("").to_string();
|
||||
s.edit(|doc, sel| {
|
||||
let before = doc.slides.len();
|
||||
let n = slidecraft_format::outline_to_slides(doc, &text);
|
||||
let n = deckcraft_format::outline_to_slides(doc, &text);
|
||||
// outline_to_slides appends; move them after the current slide.
|
||||
let at = (sel.slide + 1).min(before);
|
||||
let new: Vec<_> = doc.slides.drain(before..).collect();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Table Design and Table Layout tabs.
|
||||
|
||||
use deckcraft_model::{ShapeKind, Table};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::{ShapeKind, Table};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session};
|
||||
|
|
@ -108,7 +108,7 @@ fn with_table(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut Table) -> R
|
|||
// The edited cell may have moved out of range.
|
||||
s.select(|doc, sel| {
|
||||
let dims = super::current_shapes(doc, sel)
|
||||
.and_then(|v| slidecraft_model::find_shape(v, id))
|
||||
.and_then(|v| deckcraft_model::find_shape(v, id))
|
||||
.and_then(|x| if let ShapeKind::Table(t) = &x.kind { Some((t.n_rows(), t.n_cols())) } else { None });
|
||||
if let (Some(t), Some((nr, nc))) = (sel.text.as_mut(), dims)
|
||||
&& let Some((r, c)) = t.cell
|
||||
|
|
@ -196,8 +196,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
|
||||
fn cell_fill(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let fill =
|
||||
if bool_or(p, "none", false) { Some(slidecraft_model::Fill::None) } else { color_param(p, "color").map(slidecraft_model::Fill::solid) };
|
||||
let fill = if bool_or(p, "none", false) { Some(deckcraft_model::Fill::None) } else { color_param(p, "color").map(deckcraft_model::Fill::solid) };
|
||||
let cells: Option<Vec<(usize, usize)>> = p
|
||||
.get("cells")
|
||||
.and_then(Value::as_array)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::edit::{self as ed, Pos};
|
||||
use deckcraft_model::resolve::{self, Ctx};
|
||||
use deckcraft_model::text::{AutoFit, Run, RunKind, RunProps, TextBody};
|
||||
use deckcraft_model::{Presentation, ShapeId, ShapeKind};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::edit::{self as ed, Pos};
|
||||
use slidecraft_model::resolve::{self, Ctx};
|
||||
use slidecraft_model::text::{AutoFit, Run, RunKind, RunProps, TextBody};
|
||||
use slidecraft_model::{Presentation, ShapeId, ShapeKind};
|
||||
|
||||
use super::*;
|
||||
use crate::{DocState, Result, Selection, Session, Target, TextSel};
|
||||
|
|
@ -52,7 +52,7 @@ fn body_mut<'a>(doc: &'a mut Presentation, sel: &Selection, t: &TextSel) -> Opti
|
|||
return doc.slides.get_mut(sel.slide).map(|s| &mut Arc::make_mut(s).notes);
|
||||
}
|
||||
let shapes = crate::shapes_mut(doc, sel)?;
|
||||
let sh = slidecraft_model::find_shape_mut(shapes, t.shape)?;
|
||||
let sh = deckcraft_model::find_shape_mut(shapes, t.shape)?;
|
||||
if let Some((r, c)) = t.cell {
|
||||
if let ShapeKind::Table(tb) = &mut sh.kind {
|
||||
return tb.cell_mut(r, c).map(|x| &mut x.text);
|
||||
|
|
@ -92,14 +92,14 @@ pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
let x = xfrm_of(&doc, &sel, &sh);
|
||||
let geo = slidecraft_render::shape_geometry(&sh, x.w, x.h);
|
||||
let fields = slidecraft_text::NoFields;
|
||||
let geo = deckcraft_render::shape_geometry(&sh, x.w, x.h);
|
||||
let fields = deckcraft_text::NoFields;
|
||||
let mut nx = x;
|
||||
if bp.wrap == Some(false) {
|
||||
nx.w = slidecraft_text::fit_width(&ctx, &sh, &body, geo.text_rect, &fields).max(20.0);
|
||||
nx.w = deckcraft_text::fit_width(&ctx, &sh, &body, geo.text_rect, &fields).max(20.0);
|
||||
}
|
||||
let geo = slidecraft_render::shape_geometry(&sh, nx.w, nx.h);
|
||||
let need = slidecraft_text::fit_height(&ctx, &sh, &body, geo.text_rect, &fields);
|
||||
let geo = deckcraft_render::shape_geometry(&sh, nx.w, nx.h);
|
||||
let need = deckcraft_text::fit_height(&ctx, &sh, &body, geo.text_rect, &fields);
|
||||
let extra = (nx.h - geo.text_rect.height()).max(0.0);
|
||||
nx.h = (need + extra).max(8.0);
|
||||
if (nx.h - x.h).abs() < 0.01 && (nx.w - x.w).abs() < 0.01 && sh.xfrm.is_some() {
|
||||
|
|
@ -109,7 +109,7 @@ pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> {
|
|||
let st = s.doc_mut()?;
|
||||
let mut d = (*st.doc).clone();
|
||||
if let Some(list) = crate::shapes_mut(&mut d, &st.selection)
|
||||
&& let Some(target) = slidecraft_model::find_shape_mut(list, id)
|
||||
&& let Some(target) = deckcraft_model::find_shape_mut(list, id)
|
||||
{
|
||||
target.xfrm = Some(nx);
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ pub(crate) fn ctx_for<'a>(doc: &'a Presentation, sel: &Selection) -> Option<Ctx<
|
|||
}
|
||||
|
||||
/// 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() {
|
||||
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 ctx = ctx_for(&st.doc, &st.selection)?;
|
||||
let x = xfrm_of(&st.doc, &st.selection, sh);
|
||||
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h);
|
||||
Some(slidecraft_text::layout(
|
||||
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
|
||||
Some(deckcraft_text::layout(
|
||||
&ctx,
|
||||
sh,
|
||||
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() {
|
||||
s.edit(|doc, sel| {
|
||||
if let Some(list) = crate::shapes_mut(doc, sel)
|
||||
&& let Some(x) = slidecraft_model::find_shape_mut(list, id)
|
||||
&& let Some(x) = deckcraft_model::find_shape_mut(list, id)
|
||||
{
|
||||
x.text = Some(TextBody::default());
|
||||
}
|
||||
|
|
@ -313,7 +313,7 @@ pub(crate) fn insert_field(s: &mut Session, field: &str, text: &str) -> Result<V
|
|||
let at = ed::delete(body, a, b);
|
||||
let props = ed::props_at(body, at);
|
||||
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() } }],
|
||||
..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 body = body_of(st, &t).cloned().unwrap_or_default();
|
||||
let layout = layout_for(st, &t);
|
||||
let lp = |c: Pos| slidecraft_text::Pos { para: c.0, ch: c.1 };
|
||||
let lp = |c: Pos| deckcraft_text::Pos { para: c.0, ch: c.1 };
|
||||
let caret = t.caret;
|
||||
let collapse_to = |left: bool| if left { t.ordered().0 } else { t.ordered().1 };
|
||||
let new = match to.as_str() {
|
||||
|
|
@ -490,7 +490,7 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
};
|
||||
let Some(body) = target else { return Err(bad("text.set", "no text here (tables need `cell`)")) };
|
||||
let keep = body.paragraphs.first().and_then(|p| p.runs.first()).map(|r| r.props.clone()).unwrap_or_default();
|
||||
let levels: Vec<(u8, slidecraft_model::text::ParaProps)> = body.paragraphs.iter().map(|p| (p.level, p.props.clone())).collect();
|
||||
let levels: Vec<(u8, deckcraft_model::text::ParaProps)> = body.paragraphs.iter().map(|p| (p.level, p.props.clone())).collect();
|
||||
let mut nb = TextBody::from_text(&text);
|
||||
for (i, para) in nb.paragraphs.iter_mut().enumerate() {
|
||||
// Leading tabs set the outline level.
|
||||
|
|
@ -593,7 +593,7 @@ pub(crate) fn format_run(s: &mut Session, f: &(dyn Fn(&mut RunProps) + Send + Sy
|
|||
|
||||
/// Apply paragraph changes to the paragraphs touched by the selection (or all paragraphs of the
|
||||
/// selected shapes).
|
||||
pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::text::Paragraph) + Send + Sync)) -> Result<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()?;
|
||||
if let Some(t) = st.selection.text.clone() {
|
||||
let (a, b) = t.ordered();
|
||||
|
|
@ -639,7 +639,7 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::te
|
|||
}
|
||||
|
||||
/// Apply body (text box) changes to the edited or selected shapes.
|
||||
pub(crate) fn format_body(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::text::BodyProps) + Send + Sync)) -> Result<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 ids: Vec<ShapeId> = match &st.selection.text {
|
||||
Some(t) if !t.notes => vec![t.shape],
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::Transition;
|
||||
use deckcraft_model::anim::TRANSITIONS;
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_model::Transition;
|
||||
use slidecraft_model::anim::TRANSITIONS;
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Connectors glued to shapes: connection sites, snapping line ends to them, and rerouting attached
|
||||
//! connectors whenever the shapes they connect move, resize or rotate.
|
||||
|
||||
use slidecraft_geom::{Point, Xfrm};
|
||||
use slidecraft_model::{Presentation, Shape, ShapeId, ShapeKind};
|
||||
use deckcraft_geom::{Point, Xfrm};
|
||||
use deckcraft_model::{Presentation, Shape, ShapeId, ShapeKind};
|
||||
|
||||
use crate::Selection;
|
||||
use crate::cmd::xfrm_of;
|
||||
|
|
@ -13,7 +13,7 @@ pub fn sites(doc: &Presentation, sel: &Selection, s: &Shape) -> Vec<Point> {
|
|||
return vec![];
|
||||
}
|
||||
let x = xfrm_of(doc, sel, s);
|
||||
let mut local = slidecraft_render::shape_geometry(s, x.w, x.h).sites;
|
||||
let mut local = deckcraft_render::shape_geometry(s, x.w, x.h).sites;
|
||||
if local.is_empty() {
|
||||
local = vec![Point::new(x.w / 2.0, 0.0), Point::new(0.0, x.h / 2.0), Point::new(x.w / 2.0, x.h), Point::new(x.w, x.h / 2.0)];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ impl Session {
|
|||
let st = self.doc()?;
|
||||
let Some(t) = st.selection.text.clone() else { return Ok(Value::Null) };
|
||||
let Some((r, c)) = t.cell else { return Ok(Value::Null) };
|
||||
let Some(slidecraft_model::ShapeKind::Table(tb)) = st.shape(t.shape).map(|s| s.kind.clone()) else { return Ok(Value::Null) };
|
||||
let Some(deckcraft_model::ShapeKind::Table(tb)) = st.shape(t.shape).map(|s| s.kind.clone()) else { return Ok(Value::Null) };
|
||||
let (nr, nc) = (tb.n_rows(), tb.n_cols());
|
||||
let idx = r * nc + c;
|
||||
if !back && idx + 1 >= nr * nc {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! The SlideCraft engine façade.
|
||||
//! The DeckCraft engine façade.
|
||||
//!
|
||||
//! Every user-visible action is a command with a stable id (`slide.new`, `shape.insert`,
|
||||
//! `text.insert`, `format.bold`…) and JSON parameters. The egui UI, the CLI, the control channel
|
||||
|
|
@ -18,15 +18,15 @@ pub mod tools;
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::{Presentation, Shape, ShapeId, Slide, SlideId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use slidecraft_model::{Presentation, Shape, ShapeId, Slide, SlideId};
|
||||
|
||||
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
|
||||
pub use slidecraft_geom as geom;
|
||||
pub use slidecraft_media as media;
|
||||
pub use slidecraft_model as model;
|
||||
pub use slidecraft_render as render;
|
||||
pub use deckcraft_geom as geom;
|
||||
pub use deckcraft_media as media;
|
||||
pub use deckcraft_model as model;
|
||||
pub use deckcraft_render as render;
|
||||
pub use tools::{Mods, PointerEvent, PointerKind, ToolKind};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
@ -45,8 +45,8 @@ pub enum EngineError {
|
|||
Internal(String, String),
|
||||
}
|
||||
|
||||
impl From<slidecraft_model::ModelError> for EngineError {
|
||||
fn from(e: slidecraft_model::ModelError) -> Self {
|
||||
impl From<deckcraft_model::ModelError> for EngineError {
|
||||
fn from(e: deckcraft_model::ModelError) -> Self {
|
||||
EngineError::Other(e.to_string())
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ impl DocState {
|
|||
}
|
||||
}
|
||||
pub fn shape(&self, id: ShapeId) -> Option<&Shape> {
|
||||
slidecraft_model::find_shape(self.shapes(), id)
|
||||
deckcraft_model::find_shape(self.shapes(), id)
|
||||
}
|
||||
pub fn selected_shapes(&self) -> Vec<&Shape> {
|
||||
self.selection.shapes.iter().filter_map(|id| self.shape(*id)).collect()
|
||||
|
|
@ -282,8 +282,8 @@ pub struct Clipboard {
|
|||
pub shapes: Vec<Shape>,
|
||||
pub slides: Vec<Slide>,
|
||||
/// Media the copied shapes/slides reference.
|
||||
pub media: Vec<slidecraft_model::MediaItem>,
|
||||
pub text: Option<slidecraft_model::TextBody>,
|
||||
pub media: Vec<deckcraft_model::MediaItem>,
|
||||
pub text: Option<deckcraft_model::TextBody>,
|
||||
pub plain: String,
|
||||
/// How many times the shapes were pasted (each paste offsets further).
|
||||
pub pastes: u32,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
pub const DISCORD: &str = "https://discord.gg/artcraft";
|
||||
pub const WEBSITE: &str = "https://getartcraft.com/";
|
||||
pub const APPS: &str = "https://getartcraft.com/apps";
|
||||
pub const APP_PAGE: &str = "https://getartcraft.com/apps/slidecraft";
|
||||
pub const GITHUB: &str = "https://github.com/storytold/slidecraft";
|
||||
pub const ISSUES: &str = "https://github.com/storytold/slidecraft/issues";
|
||||
pub const APP_PAGE: &str = "https://getartcraft.com/apps/deckcraft";
|
||||
pub const GITHUB: &str = "https://github.com/storytold/deckcraft";
|
||||
pub const ISSUES: &str = "https://github.com/storytold/deckcraft/issues";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! `file.recovery.open` reopens what was left there as unsaved presentations that remember where
|
||||
//! 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).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -17,16 +17,16 @@ use crate::{DocState, EngineError, Result, Session};
|
|||
pub fn default_dir() -> Option<PathBuf> {
|
||||
let home = std::env::var_os("HOME").map(PathBuf::from);
|
||||
if cfg!(target_os = "macos") {
|
||||
return home.map(|h| h.join("Library/Application Support/SlideCraft/Recovery"));
|
||||
return home.map(|h| h.join("Library/Application Support/DeckCraft/Recovery"));
|
||||
}
|
||||
if cfg!(target_os = "windows") {
|
||||
return std::env::var_os("APPDATA").map(|a| PathBuf::from(a).join("SlideCraft").join("Recovery"));
|
||||
return std::env::var_os("APPDATA").map(|a| PathBuf::from(a).join("DeckCraft").join("Recovery"));
|
||||
}
|
||||
std::env::var_os("XDG_DATA_HOME").map(PathBuf::from).or_else(|| home.map(|h| h.join(".local/share"))).map(|d| d.join("slidecraft/recovery"))
|
||||
std::env::var_os("XDG_DATA_HOME").map(PathBuf::from).or_else(|| home.map(|h| h.join(".local/share"))).map(|d| d.join("deckcraft/recovery"))
|
||||
}
|
||||
|
||||
fn files(dir: &Path, uid: u64) -> (PathBuf, PathBuf) {
|
||||
(dir.join(format!("{uid}.slidecraft")), dir.join(format!("{uid}.json")))
|
||||
(dir.join(format!("{uid}.deckcraft")), dir.join(format!("{uid}.json")))
|
||||
}
|
||||
|
||||
fn now_secs() -> u64 {
|
||||
|
|
@ -58,7 +58,7 @@ pub fn save(s: &Session, dir: &Path) -> Result<usize> {
|
|||
continue;
|
||||
}
|
||||
let (doc, meta) = files(dir, d.uid);
|
||||
let bytes = slidecraft_format::save(&d.doc).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let bytes = deckcraft_format::save(&d.doc).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
// Write then rename, so a crash mid-write never leaves a torn file.
|
||||
let tmp = doc.with_extension("tmp");
|
||||
std::fs::write(&tmp, &bytes).and_then(|_| std::fs::rename(&tmp, &doc)).map_err(|e| EngineError::Other(format!("{}: {e}", doc.display())))?;
|
||||
|
|
@ -75,7 +75,7 @@ pub fn list(dir: &Path) -> Vec<(u64, Value)> {
|
|||
let Ok(rd) = std::fs::read_dir(dir) else { return out };
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.extension().and_then(|x| x.to_str()) != Some("slidecraft") {
|
||||
if p.extension().and_then(|x| x.to_str()) != Some("deckcraft") {
|
||||
continue;
|
||||
}
|
||||
let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::<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) {
|
||||
let (doc, _) = files(dir, uid);
|
||||
let Ok(bytes) = std::fs::read(&doc) else { continue };
|
||||
let Ok(mut d) = slidecraft_format::load(&bytes) else { continue };
|
||||
slidecraft_format::repair(&mut d);
|
||||
let Ok(mut d) = deckcraft_format::load(&bytes) else { continue };
|
||||
deckcraft_format::repair(&mut d);
|
||||
let path = meta.get("path").and_then(Value::as_str).map(str::to_string);
|
||||
let title = meta.get("title").and_then(Value::as_str).filter(|t| !t.is_empty()).unwrap_or("Presentation");
|
||||
let mut st = DocState::new(d, path, format!("{title} (Recovered)"));
|
||||
|
|
@ -117,7 +117,7 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
fn temp(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("slidecraft-{name}-{}", std::process::id()));
|
||||
let dir = std::env::temp_dir().join(format!("deckcraft-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
dir
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ mod tests {
|
|||
// Saving drops the entry.
|
||||
s.recovery_dir = Some(dir.clone());
|
||||
assert_eq!(save(&s, &dir).unwrap(), 1);
|
||||
let path = dir.join("saved.slidecraft");
|
||||
let path = dir.join("saved.deckcraft");
|
||||
s.execute("file.saveAs", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert!(list(&dir).is_empty());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
|
|
@ -153,7 +153,7 @@ mod tests {
|
|||
fn corrupt_entries_are_skipped() {
|
||||
let dir = temp("recovery-corrupt");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("7.slidecraft"), b"not a presentation").unwrap();
|
||||
std::fs::write(dir.join("7.deckcraft"), b"not a presentation").unwrap();
|
||||
std::fs::write(dir.join("7.json"), b"[1, 2]").unwrap();
|
||||
let mut s = Session::new();
|
||||
assert!(open(&mut s, &dir).unwrap().is_empty());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! A sample deck built entirely in code (our own text, shapes and data), for demos, screenshots
|
||||
//! and tests: `slidecraft --sample`.
|
||||
//! and tests: `deckcraft --sample`.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -15,13 +15,13 @@ fn last_id(v: &Value) -> u64 {
|
|||
|
||||
/// Open the sample presentation in a new document.
|
||||
pub fn open_sample(s: &mut Session) -> Result<()> {
|
||||
run(s, "file.new", json!({"theme": "SlideCraft"}))?;
|
||||
run(s, "file.new", json!({"theme": "DeckCraft"}))?;
|
||||
let st = s.doc()?;
|
||||
let title = st.current_slide().and_then(|x| x.shapes.first()).map(|x| x.id.0).unwrap_or(0);
|
||||
let sub = st.current_slide().and_then(|x| x.shapes.get(1)).map(|x| x.id.0).unwrap_or(0);
|
||||
run(s, "text.set", json!({"id": title, "text": "Northern Lights"}))?;
|
||||
run(s, "text.set", json!({"id": sub, "text": "A short tour of the aurora — where it comes from, when to look, and how to photograph it"}))?;
|
||||
run(s, "slide.notes", json!({"text": "Welcome everyone. This deck was made in SlideCraft."}))?;
|
||||
run(s, "slide.notes", json!({"text": "Welcome everyone. This deck was made in DeckCraft."}))?;
|
||||
// Decorative bands on the title slide.
|
||||
let b = run(s, "shape.insert", json!({"preset": "rect", "rect": [0, 470, 960, 70]}))?;
|
||||
run(s, "shape.fill", json!({"id": last_id(&b), "gradient": {"stops": [[0, "accent1"], [0.5, "accent4"], [1, "accent2"]], "angle": 0}}))?;
|
||||
|
|
@ -114,7 +114,7 @@ pub fn open_sample(s: &mut Session) -> Result<()> {
|
|||
run(s, "transition.set", json!({"kind": "split"}))?;
|
||||
|
||||
// Closing.
|
||||
run(s, "slide.new", json!({"layout": "title", "title": "Clear skies!", "body": "Made with SlideCraft — free and open source"}))?;
|
||||
run(s, "slide.new", json!({"layout": "title", "title": "Clear skies!", "body": "Made with DeckCraft — free and open source"}))?;
|
||||
let moon = run(s, "shape.insert", json!({"preset": "moon", "rect": [820, 40, 80, 120]}))?;
|
||||
run(s, "shape.fill", json!({"id": last_id(&moon), "color": "accent6"}))?;
|
||||
run(s, "shape.line", json!({"id": last_id(&moon), "none": true}))?;
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ fn sample_deck_builds_and_renders() {
|
|||
assert!(st.doc.validate().is_empty());
|
||||
assert!(!st.is_dirty());
|
||||
for i in 0..st.doc.slides.len() {
|
||||
let img = slidecraft_render::render_slide(&st.doc, i, &slidecraft_render::RenderOpts { scale: 0.2, ..Default::default() });
|
||||
let img = deckcraft_render::render_slide(&st.doc, i, &deckcraft_render::RenderOpts { scale: 0.2, ..Default::default() });
|
||||
assert_eq!(img.width, 192);
|
||||
}
|
||||
let v = s.execute("document.inspect", &json!({})).unwrap();
|
||||
|
|
@ -187,7 +187,7 @@ fn save_and_reopen_native() {
|
|||
sample::open_sample(&mut s).unwrap();
|
||||
let bytes = s.execute("file.saveBytes", &json!({})).unwrap()["data"].as_str().unwrap().to_string();
|
||||
let n = s.doc().unwrap().doc.slides.len();
|
||||
s.execute("file.openBytes", &json!({"name": "x.slidecraft", "data": bytes})).unwrap();
|
||||
s.execute("file.openBytes", &json!({"name": "x.deckcraft", "data": bytes})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.slides.len(), n);
|
||||
assert_eq!(s.documents().len(), 2);
|
||||
}
|
||||
|
|
@ -249,9 +249,9 @@ fn themes_and_slide_size() {
|
|||
assert!(v["shapes"].as_array().unwrap().iter().any(|x| x["placeholder"] == "sldNum"));
|
||||
}
|
||||
|
||||
fn line_ends(s: &Session, id: u64) -> (slidecraft_geom::Point, slidecraft_geom::Point) {
|
||||
fn line_ends(s: &Session, id: u64) -> (deckcraft_geom::Point, deckcraft_geom::Point) {
|
||||
let st = s.doc().unwrap();
|
||||
let sh = st.shape(slidecraft_model::ShapeId(id as u32)).unwrap();
|
||||
let sh = st.shape(deckcraft_model::ShapeId(id as u32)).unwrap();
|
||||
connect::endpoints(&cmd::xfrm_of(&st.doc, &st.selection, sh))
|
||||
}
|
||||
|
||||
|
|
@ -271,7 +271,7 @@ fn connectors_follow_glued_shapes() {
|
|||
s.execute("shape.move", &json!({"dx": 50, "dy": 20})).unwrap();
|
||||
let (q0, q1) = line_ends(&s, c);
|
||||
assert!((q0 - p0).hypot() < 1e-6);
|
||||
assert!(((q1 - p1) - slidecraft_geom::Vec2::new(50.0, 20.0)).hypot() < 1e-6, "{p1:?} → {q1:?}");
|
||||
assert!(((q1 - p1) - deckcraft_geom::Vec2::new(50.0, 20.0)).hypot() < 1e-6, "{p1:?} → {q1:?}");
|
||||
// Undo puts both back in one step.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!((line_ends(&s, c).1 - p1).hypot() < 1e-6);
|
||||
|
|
@ -279,8 +279,8 @@ fn connectors_follow_glued_shapes() {
|
|||
s.execute("edit.select", &json!({"ids": [a]})).unwrap();
|
||||
s.execute("edit.delete", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let sh = st.shape(slidecraft_model::ShapeId(c as u32)).unwrap();
|
||||
assert!(matches!(sh.kind, slidecraft_model::ShapeKind::Connector { start: None, end: Some(_) }));
|
||||
let sh = st.shape(deckcraft_model::ShapeId(c as u32)).unwrap();
|
||||
assert!(matches!(sh.kind, deckcraft_model::ShapeKind::Connector { start: None, end: Some(_) }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -301,7 +301,7 @@ fn drawing_a_line_between_shapes_glues_it() {
|
|||
let st = s.doc().unwrap();
|
||||
let line = st.shapes().iter().find(|x| x.is_line()).unwrap();
|
||||
match line.kind {
|
||||
slidecraft_model::ShapeKind::Connector { start: Some((x, _)), end: Some((y, _)) } => {
|
||||
deckcraft_model::ShapeKind::Connector { start: Some((x, _)), end: Some((y, _)) } => {
|
||||
assert_eq!((x.0 as u64, y.0 as u64), (a, b));
|
||||
}
|
||||
ref k => panic!("not glued: {k:?}"),
|
||||
|
|
@ -336,7 +336,7 @@ fn freeform_tools_and_command() {
|
|||
let st = s.doc().unwrap();
|
||||
let sh = st.shapes().last().unwrap();
|
||||
match &sh.geom {
|
||||
slidecraft_model::Geom::Custom { paths } => assert!(paths[0].d.ends_with('Z') && paths[0].d.matches(" L ").count() == 2, "{}", paths[0].d),
|
||||
deckcraft_model::Geom::Custom { paths } => assert!(paths[0].d.ends_with('Z') && paths[0].d.matches(" L ").count() == 2, "{}", paths[0].d),
|
||||
g => panic!("{g:?}"),
|
||||
}
|
||||
let x = sh.xfrm.unwrap();
|
||||
|
|
@ -359,10 +359,10 @@ fn freeform_tools_and_command() {
|
|||
s.pointer(ev(tools::PointerKind::Up, x, y)).unwrap();
|
||||
}
|
||||
let r = s.key("Enter", Default::default()).unwrap();
|
||||
let id = slidecraft_model::ShapeId(r["id"].as_u64().unwrap() as u32);
|
||||
let id = deckcraft_model::ShapeId(r["id"].as_u64().unwrap() as u32);
|
||||
let st = s.doc().unwrap();
|
||||
match &st.shape(id).unwrap().geom {
|
||||
slidecraft_model::Geom::Custom { paths } => assert_eq!(paths[0].d.matches(" C ").count(), 3),
|
||||
deckcraft_model::Geom::Custom { paths } => assert_eq!(paths[0].d.matches(" C ").count(), 3),
|
||||
g => panic!("{g:?}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_geom::{Affine, Point, Rect, Vec2, Xfrm};
|
||||
use deckcraft_model::{Shape, ShapeId, ShapeKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use slidecraft_geom::{Affine, Point, Rect, Vec2, Xfrm};
|
||||
use slidecraft_model::{Shape, ShapeId, ShapeKind};
|
||||
|
||||
use crate::cmd::{self, xfrm_of};
|
||||
use crate::{HistoryEntry, Result, Session, TextSel};
|
||||
|
|
@ -55,7 +55,7 @@ pub enum ToolKind {
|
|||
/// Ink: pen (`pen`, `highlighter`) or `eraser`.
|
||||
Ink {
|
||||
mode: String,
|
||||
color: slidecraft_color::Rgba,
|
||||
color: deckcraft_color::Rgba,
|
||||
width: f64,
|
||||
},
|
||||
}
|
||||
|
|
@ -229,7 +229,7 @@ impl Session {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h);
|
||||
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
|
||||
for hd in &geo.handles {
|
||||
if (x.affine() * hd.pos - p).hypot() <= tol * 1.6 {
|
||||
return Some(Hit::Adjust { id: *id, index: hd.index });
|
||||
|
|
@ -277,7 +277,7 @@ impl Session {
|
|||
return Ok(Value::Null);
|
||||
}
|
||||
self.tool.hover = self.hit_test(p, tol);
|
||||
let line_tool = matches!(&self.tool.kind, ToolKind::Shape { preset } if slidecraft_geom::preset::is_line_like(preset));
|
||||
let line_tool = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset));
|
||||
if line_tool {
|
||||
self.update_sites(p, tol, None);
|
||||
} else {
|
||||
|
|
@ -485,7 +485,7 @@ impl Session {
|
|||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?;
|
||||
for (id, o) in &origs2 {
|
||||
if let Some(sh) = slidecraft_model::find_shape_mut(list, *id) {
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, *id) {
|
||||
sh.xfrm = Some(Xfrm { x: o.x + d.x, y: o.y + d.y, ..*o });
|
||||
}
|
||||
}
|
||||
|
|
@ -556,7 +556,7 @@ impl Session {
|
|||
let st = self.doc()?;
|
||||
let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) };
|
||||
let local = orig.affine().inverse() * p;
|
||||
let geo = slidecraft_render::shape_geometry(&sh, orig.w, orig.h);
|
||||
let geo = deckcraft_render::shape_geometry(&sh, orig.w, orig.h);
|
||||
let Some(h) = geo.handles.iter().find(|h| h.index == index) else { return Ok(Value::Null) };
|
||||
let free2d = matches!(sh.geom.preset_name(), Some(n) if n.contains("Callout") && !n.contains("Arrow"));
|
||||
let vals: Vec<(usize, f64)> = if free2d {
|
||||
|
|
@ -568,10 +568,10 @@ impl Session {
|
|||
};
|
||||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?;
|
||||
if let Some(sh) = slidecraft_model::find_shape_mut(list, id)
|
||||
&& let slidecraft_model::Geom::Preset { name, adj } = &mut sh.geom
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, id)
|
||||
&& let deckcraft_model::Geom::Preset { name, adj } = &mut sh.geom
|
||||
{
|
||||
let defaults = slidecraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
|
||||
let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
|
||||
for (i, v) in &vals {
|
||||
while adj.len() <= *i {
|
||||
let k = adj.len();
|
||||
|
|
@ -589,7 +589,7 @@ impl Session {
|
|||
self.tool.marquee = Some(Rect::from_points(start, p));
|
||||
}
|
||||
Drag::Create { start } => {
|
||||
let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if slidecraft_geom::preset::is_line_like(preset));
|
||||
let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset));
|
||||
let x = if line {
|
||||
let mut q = if mods.shift { constrain_45(start, p) } else { p };
|
||||
if let Some((_, _, at)) = self.update_sites(q, tol, None) {
|
||||
|
|
@ -693,7 +693,7 @@ impl Session {
|
|||
let dragged = (p - start).hypot() > 2.0;
|
||||
let res = match &kind {
|
||||
ToolKind::Shape { preset } => {
|
||||
let line = slidecraft_geom::preset::is_line_like(preset);
|
||||
let line = deckcraft_geom::preset::is_line_like(preset);
|
||||
let rect = match preview.filter(|_| dragged) {
|
||||
Some(x) => x,
|
||||
None if line => Xfrm::new(start.x, start.y, 72.0, 0.0),
|
||||
|
|
@ -789,7 +789,7 @@ impl Session {
|
|||
let x = xfrm_of(&st.doc, &st.selection, &sh);
|
||||
let local = x.affine().inverse() * p;
|
||||
if let ShapeKind::Table(t) = &sh.kind {
|
||||
let cell = slidecraft_render_cell(t, local);
|
||||
let cell = deckcraft_render_cell(t, local);
|
||||
return self.execute("text.edit", &json!({"id": id, "cell": [cell.0, cell.1]}));
|
||||
}
|
||||
Ok(Value::Null)
|
||||
|
|
@ -857,7 +857,7 @@ impl Session {
|
|||
fn set_xfrm(&mut self, id: ShapeId, x: Xfrm) -> Result<()> {
|
||||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("tool", "no slide"))?;
|
||||
if let Some(sh) = slidecraft_model::find_shape_mut(list, id) {
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, id) {
|
||||
sh.xfrm = Some(x);
|
||||
if let ShapeKind::Table(t) = &mut sh.kind {
|
||||
// Tables resize their columns and rows proportionally.
|
||||
|
|
@ -875,7 +875,7 @@ impl Session {
|
|||
self.execute("shape.setBounds", &json!({"id": id, "x": x.x, "y": x.y, "w": x.w, "h": x.h}))?;
|
||||
if x.flip_h || x.flip_v {
|
||||
self.edit(|doc, sel| {
|
||||
if let Some(sh) = crate::shapes_mut(doc, sel).and_then(|l| slidecraft_model::find_shape_mut(l, id))
|
||||
if let Some(sh) = crate::shapes_mut(doc, sel).and_then(|l| deckcraft_model::find_shape_mut(l, id))
|
||||
&& let Some(xx) = sh.xfrm.as_mut()
|
||||
{
|
||||
xx.flip_h = x.flip_h;
|
||||
|
|
@ -969,14 +969,14 @@ impl Session {
|
|||
let rr = r + Vec2::new(d, 0.0);
|
||||
guides.push(Guide { vertical: true, pos: x, from: b.y0.min(rr.y0), to: b.y1.max(rr.y1) });
|
||||
} else if self.prefs.snap_to_grid {
|
||||
off.x = slidecraft_geom::snap(r.x0, self.prefs.grid_spacing) - r.x0;
|
||||
off.x = deckcraft_geom::snap(r.x0, self.prefs.grid_spacing) - r.x0;
|
||||
}
|
||||
if let Some((d, y, b)) = best_y {
|
||||
off.y = d;
|
||||
let rr = r + Vec2::new(0.0, d);
|
||||
guides.push(Guide { vertical: false, pos: y, from: b.x0.min(rr.x0), to: b.x1.max(rr.x1) });
|
||||
} else if self.prefs.snap_to_grid {
|
||||
off.y = slidecraft_geom::snap(r.y0, self.prefs.grid_spacing) - r.y0;
|
||||
off.y = deckcraft_geom::snap(r.y0, self.prefs.grid_spacing) - r.y0;
|
||||
}
|
||||
(off, guides)
|
||||
}
|
||||
|
|
@ -1014,7 +1014,7 @@ impl Session {
|
|||
|
||||
fn add_ink(&mut self, points: Vec<(f64, f64, f32)>) -> Result<Value> {
|
||||
let ToolKind::Ink { mode, color, width } = self.tool.kind.clone() else { return Ok(Value::Null) };
|
||||
let stroke = slidecraft_model::InkStroke { points, color, width, highlighter: mode == "highlighter" };
|
||||
let stroke = deckcraft_model::InkStroke { points, color, width, highlighter: mode == "highlighter" };
|
||||
// Strokes join the slide's ink layer (one ink shape per slide).
|
||||
let existing = self.doc()?.shapes().iter().rev().find(|s| matches!(s.kind, ShapeKind::Ink { .. })).map(|s| s.id);
|
||||
self.edit(|doc, sel| {
|
||||
|
|
@ -1035,7 +1035,7 @@ impl Session {
|
|||
}
|
||||
};
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("ink", "no slide"))?;
|
||||
if let Some(sh) = slidecraft_model::find_shape_mut(list, id)
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, id)
|
||||
&& let ShapeKind::Ink { strokes } = &mut sh.kind
|
||||
{
|
||||
strokes.push(stroke);
|
||||
|
|
@ -1051,7 +1051,7 @@ impl Session {
|
|||
let sel_before = st.selection.clone();
|
||||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("ink", "no slide"))?;
|
||||
if let Some(sh) = slidecraft_model::find_shape_mut(list, ink)
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(list, ink)
|
||||
&& let ShapeKind::Ink { strokes } = &mut sh.kind
|
||||
{
|
||||
strokes.retain(|s| !s.points.iter().any(|(x, y, _)| (Point::new(*x, *y) - p).hypot() <= tol * 2.0 + s.width));
|
||||
|
|
@ -1186,7 +1186,7 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
|
|||
ShapeKind::Group { children, child } => {
|
||||
// A group is hit when any child is hit; the group is what gets selected.
|
||||
let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
|
||||
let m = slidecraft_geom::group_child_affine(&x, ch);
|
||||
let m = deckcraft_geom::group_child_affine(&x, ch);
|
||||
let q = m.inverse() * p;
|
||||
for c in children.iter().rev() {
|
||||
if let Some(cx) = c.xfrm {
|
||||
|
|
@ -1208,9 +1208,9 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
|
|||
}
|
||||
_ => {
|
||||
if sh.is_line() {
|
||||
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h);
|
||||
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
|
||||
let lw = sh.line.as_ref().and_then(|l| l.width).unwrap_or(1.0);
|
||||
if slidecraft_geom::near_path(&geo.outline(), local, tol.max(lw)) {
|
||||
if deckcraft_geom::near_path(&geo.outline(), local, tol.max(lw)) {
|
||||
return Some(Hit::Shape { id: sh.id, text: false });
|
||||
}
|
||||
return None;
|
||||
|
|
@ -1221,11 +1221,11 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<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);
|
||||
// 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 geo = slidecraft_render::shape_geometry(sh, x.w, x.h);
|
||||
let filled = !matches!(sh.fill, Some(deckcraft_model::Fill::None)) || sh.style.is_some();
|
||||
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
|
||||
let in_geo = {
|
||||
use slidecraft_geom::KurboShape;
|
||||
geo.outline().winding(local) != 0 || slidecraft_geom::near_path(&geo.outline(), local, tol)
|
||||
use deckcraft_geom::KurboShape;
|
||||
geo.outline().winding(local) != 0 || deckcraft_geom::near_path(&geo.outline(), local, tol)
|
||||
};
|
||||
if has_text
|
||||
|| 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 { .. }
|
||||
)
|
||||
|| (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) });
|
||||
}
|
||||
|
|
@ -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.
|
||||
fn slidecraft_render_cell(t: &slidecraft_model::Table, p: Point) -> (usize, usize) {
|
||||
fn deckcraft_render_cell(t: &deckcraft_model::Table, p: Point) -> (usize, usize) {
|
||||
let mut y = 0.0;
|
||||
let mut row = t.rows.len().saturating_sub(1);
|
||||
for (i, r) in t.rows.iter().enumerate() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "slidecraft-fonts"
|
||||
name = "deckcraft-fonts"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "SlideCraft fonts: font database (craft-fonts + system), substitutes for Office fonts, metrics, outlines, shaping"
|
||||
description = "DeckCraft fonts: font database (craft-fonts + system), substitutes for Office fonts, metrics, outlines, shaping"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
use std::fmt::Write as _;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Families SlideCraft never embeds (AGENTS.md §1: no Adobe-authored visual design).
|
||||
/// Families DeckCraft never embeds (AGENTS.md §1: no Adobe-authored visual design).
|
||||
const EXCLUDED_FAMILIES: &[&str] = &["Source Sans 3", "Source Serif 4", "Source Han Sans", "Source Han Serif", "Noto Sans CJK SC"];
|
||||
|
||||
fn main() {
|
||||
|
|
@ -53,7 +53,7 @@ fn manifest(dir: &std::path::Path, manifest: &std::path::Path) -> Result<String,
|
|||
let [family, style, file, scripts, ..] = f.as_slice() else {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//! OpenType shaping.
|
||||
//!
|
||||
//! 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.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
[package]
|
||||
name = "slidecraft-format"
|
||||
name = "deckcraft-format"
|
||||
version.workspace = true
|
||||
edition.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]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
slidecraft-model = { workspace = true }
|
||||
deckcraft-model = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//! fields, so newer files open in older builds with what those builds understand.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
|
@ -7,11 +7,13 @@
|
|||
use std::io::{Cursor, Read, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use slidecraft_model::text::{Paragraph, TextBody};
|
||||
use slidecraft_model::{LayoutType, Presentation, defaults};
|
||||
use deckcraft_model::text::{Paragraph, TextBody};
|
||||
use deckcraft_model::{LayoutType, Presentation, defaults};
|
||||
|
||||
pub const MIMETYPE: &str = "application/vnd.storyteller.slidecraft+zip";
|
||||
pub const EXTENSION: &str = "slidecraft";
|
||||
pub const MIMETYPE: &str = "application/vnd.storyteller.deckcraft+zip";
|
||||
/// Files written before the SlideCraft → DeckCraft rename (`.slidecraft`) are still read.
|
||||
pub const LEGACY_MIMETYPE: &str = concat!("application/vnd.storyteller.", "slide", "craft+zip");
|
||||
pub const EXTENSION: &str = "deckcraft";
|
||||
/// Format version written into the manifest.
|
||||
pub const VERSION: u32 = 1;
|
||||
/// Largest entry we will inflate (guards against zip bombs).
|
||||
|
|
@ -19,7 +21,7 @@ const MAX_ENTRY: u64 = 2 << 30;
|
|||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FormatError {
|
||||
#[error("not a SlideCraft file: {0}")]
|
||||
#[error("not a DeckCraft file: {0}")]
|
||||
NotOurs(String),
|
||||
#[error("damaged file: {0}")]
|
||||
Damaged(String),
|
||||
|
|
@ -48,9 +50,9 @@ pub fn save(p: &Presentation) -> Result<Vec<u8>> {
|
|||
z.start_file("mimetype", stored).map_err(w)?;
|
||||
z.write_all(MIMETYPE.as_bytes()).map_err(|e| FormatError::Write(e.to_string()))?;
|
||||
let m = Manifest {
|
||||
format: "slidecraft".into(),
|
||||
format: "deckcraft".into(),
|
||||
version: VERSION,
|
||||
generator: format!("SlideCraft {}", env!("CARGO_PKG_VERSION")),
|
||||
generator: format!("DeckCraft {}", env!("CARGO_PKG_VERSION")),
|
||||
presentation: p.clone(),
|
||||
};
|
||||
let json = serde_json::to_vec(&m).map_err(|e| FormatError::Write(e.to_string()))?;
|
||||
|
|
@ -79,9 +81,9 @@ fn is_compressed(ct: &str) -> bool {
|
|||
|| ct.contains("zip")
|
||||
}
|
||||
|
||||
/// Does this look like a `.slidecraft` file?
|
||||
/// Does this look like a `.deckcraft` file?
|
||||
pub fn sniff(bytes: &[u8]) -> bool {
|
||||
bytes.starts_with(b"PK") && bytes.windows(MIMETYPE.len()).take(200).any(|w| w == MIMETYPE.as_bytes())
|
||||
bytes.starts_with(b"PK") && [MIMETYPE, LEGACY_MIMETYPE].iter().any(|m| bytes.windows(m.len()).take(200).any(|w| w == m.as_bytes()))
|
||||
}
|
||||
|
||||
fn read_entry(z: &mut zip::ZipArchive<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 json = read_entry(&mut z, "presentation.json").map_err(|_| FormatError::NotOurs("no presentation.json".into()))?;
|
||||
let m: Manifest = serde_json::from_slice(&json).map_err(|e| FormatError::Damaged(e.to_string()))?;
|
||||
if m.format != "slidecraft" {
|
||||
if m.format != "deckcraft" {
|
||||
return Err(FormatError::NotOurs(m.format));
|
||||
}
|
||||
let mut p = m.presentation;
|
||||
|
|
@ -126,7 +128,7 @@ pub fn repair(p: &mut Presentation) {
|
|||
let needs_layout = p.slides.get(i).is_some_and(|s| p.layout(s.layout).is_none());
|
||||
let dup = p.slides.get(i).is_some_and(|s| !seen.insert(s.id));
|
||||
if needs_layout || dup {
|
||||
let new_id = if dup { Some(slidecraft_model::SlideId(p.alloc_id())) } else { None };
|
||||
let new_id = if dup { Some(deckcraft_model::SlideId(p.alloc_id())) } else { None };
|
||||
if let Some(s) = p.slides.get_mut(i) {
|
||||
let s = Arc::make_mut(s);
|
||||
if needs_layout {
|
||||
|
|
@ -150,8 +152,8 @@ pub fn repair(p: &mut Presentation) {
|
|||
pub fn outline_to_slides(p: &mut Presentation, text: &str) -> usize {
|
||||
let Some(lay) = defaults::layout_of_kind(p, LayoutType::TitleAndContent) else { return 0 };
|
||||
let mut made = 0;
|
||||
let mut cur: Option<slidecraft_model::Slide> = None;
|
||||
let flush = |p: &mut Presentation, s: Option<slidecraft_model::Slide>, made: &mut usize| {
|
||||
let mut cur: Option<deckcraft_model::Slide> = None;
|
||||
let flush = |p: &mut Presentation, s: Option<deckcraft_model::Slide>, made: &mut usize| {
|
||||
if let Some(s) = s {
|
||||
p.slides.push(Arc::new(s));
|
||||
*made += 1;
|
||||
|
|
@ -242,7 +244,7 @@ mod tests {
|
|||
#[test]
|
||||
fn repair_fixes_missing_layout_and_size() {
|
||||
let mut p = Presentation::default();
|
||||
Arc::make_mut(&mut p.slides[0]).layout = slidecraft_model::LayoutId(99999);
|
||||
Arc::make_mut(&mut p.slides[0]).layout = deckcraft_model::LayoutId(99999);
|
||||
p.slide_size.width = f64::NAN;
|
||||
repair(&mut p);
|
||||
assert!(p.validate().is_empty());
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "slidecraft-geom"
|
||||
name = "deckcraft-geom"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "SlideCraft geometry: units (EMU/pt), transforms, preset shape generators, adjust handles, hit testing"
|
||||
description = "DeckCraft geometry: units (EMU/pt), transforms, preset shape generators, adjust handles, hit testing"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! SlideCraft geometry: units, shape transforms and preset shape generators.
|
||||
//! DeckCraft geometry: units, shape transforms and preset shape generators.
|
||||
//!
|
||||
//! The document model keeps positions in points (1/72 inch) as `f64`. Office Open XML stores EMU
|
||||
//! (English Metric Units, 12 700 per point); [`emu_to_pt`] and [`pt_to_emu`] convert losslessly for
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "slidecraft-h264"
|
||||
name = "deckcraft-h264"
|
||||
description = "Clean-room pure-Rust H.264/AVC decoder (ITU-T H.264)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
|
@ -14,6 +14,6 @@ default = ["threads"]
|
|||
threads = ["dep:rayon"]
|
||||
|
||||
[dependencies]
|
||||
slidecraft-bitstream = { workspace = true }
|
||||
deckcraft-bitstream = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rayon = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::cavlc_tables::*;
|
||||
use crate::error::{Result, ensure};
|
||||
use slidecraft_bitstream::BitReader;
|
||||
use deckcraft_bitstream::BitReader;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Prefix-code lookup keyed by (number of leading zeros, next 5 bits after the first one bit).
|
||||
|
|
@ -182,7 +182,7 @@ pub(crate) fn residual_block(r: &mut BitReader, coeff: &mut [i32], start_idx: us
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use slidecraft_bitstream::BitWriter;
|
||||
use deckcraft_bitstream::BitWriter;
|
||||
|
||||
fn kraft(codes: &[&str]) -> f64 {
|
||||
codes.iter().filter(|c| !c.is_empty()).map(|c| 0.5f64.powi(c.len() as i32)).sum()
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ mod tests {
|
|||
mod speed {
|
||||
use super::*;
|
||||
|
||||
/// Rough timing of the edge filters (`cargo test --release -p slidecraft-h264 --lib edge_filter_speed -- --ignored --nocapture`).
|
||||
/// Rough timing of the edge filters (`cargo test --release -p deckcraft-h264 --lib edge_filter_speed -- --ignored --nocapture`).
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn edge_filter_speed() {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use crate::slice::{NalHeader, Poc, PocState, SliceHeader, SliceType, nal_type};
|
|||
use crate::slicedec::{PicState, SliceDecoder};
|
||||
use crate::transform::LevelScale;
|
||||
use crate::{ColorInfo, Picture};
|
||||
use slidecraft_bitstream::{BitReader, annexb_nals, length_prefixed_nals, unescape_rbsp};
|
||||
use deckcraft_bitstream::{BitReader, annexb_nals, length_prefixed_nals, unescape_rbsp};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use slidecraft_bitstream::BitError;
|
||||
use deckcraft_bitstream::BitError;
|
||||
|
||||
/// Errors produced by the decoder.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
//! multiple slices, long-term references and all MMCOs, deblocking.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! let mut dec = slidecraft_h264::Decoder::new();
|
||||
//! let mut dec = deckcraft_h264::Decoder::new();
|
||||
//! let stream = std::fs::read("video.h264").unwrap();
|
||||
//! for pic in dec.decode(&stream, 0).unwrap() {
|
||||
//! println!("{}x{} poc {}", pic.width, pic.height, pic.poc);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::error::{Result, ensure, unsupported};
|
||||
use crate::tables::*;
|
||||
use slidecraft_bitstream::BitReader;
|
||||
use deckcraft_bitstream::BitReader;
|
||||
|
||||
/// Scaling matrices after applying fall-back rules, stored in raster order (weightScale).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -665,7 +665,7 @@ impl Pps {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use slidecraft_bitstream::BitWriter;
|
||||
use deckcraft_bitstream::BitWriter;
|
||||
|
||||
#[test]
|
||||
fn scaling_list_default_signal() {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::error::{Result, ensure};
|
||||
use crate::params::{Pps, Sps};
|
||||
use slidecraft_bitstream::BitReader;
|
||||
use deckcraft_bitstream::BitReader;
|
||||
|
||||
/// NAL unit types used by the decoder.
|
||||
pub mod nal_type {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::picture::*;
|
|||
use crate::slice::{SliceHeader, SliceType};
|
||||
use crate::tables::*;
|
||||
use crate::transform::{self, LevelScale};
|
||||
use slidecraft_bitstream::BitReader;
|
||||
use deckcraft_bitstream::BitReader;
|
||||
|
||||
/// Per-slice data kept with the picture (deblocking, motion field export).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
use crate::Decoder;
|
||||
use crate::cabac::{Cabac, NEXT_STATE, RANGE_TAB_LPS};
|
||||
use slidecraft_bitstream::{BitWriter, escape_rbsp};
|
||||
use deckcraft_bitstream::{BitWriter, escape_rbsp};
|
||||
|
||||
const MB_W: usize = 4;
|
||||
const MB_H: usize = 3;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "slidecraft-hevc"
|
||||
name = "deckcraft-hevc"
|
||||
description = "Clean-room pure-Rust H.265/HEVC decoder (ITU-T H.265)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
|
@ -14,6 +14,6 @@ default = ["threads"]
|
|||
threads = ["dep:rayon"]
|
||||
|
||||
[dependencies]
|
||||
slidecraft-bitstream = { workspace = true }
|
||||
deckcraft-bitstream = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rayon = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::picture::{Frame, FrameRef};
|
|||
use crate::slice::{NalHeader, SliceHeader, nal_type};
|
||||
use crate::slicedec::{F_INTRA, F_SKIP, PicState, SliceDecoder, SliceJob};
|
||||
use crate::{ColorInfo, Picture, Plane};
|
||||
use slidecraft_bitstream::{annexb_nals, length_prefixed_nals, unescape_rbsp};
|
||||
use deckcraft_bitstream::{annexb_nals, length_prefixed_nals, unescape_rbsp};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use slidecraft_bitstream::BitError;
|
||||
use deckcraft_bitstream::BitError;
|
||||
|
||||
/// Errors produced by the decoder.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
//! (parsing), dependent slice segments, short- and long-term reference picture sets.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! let mut dec = slidecraft_hevc::Decoder::new();
|
||||
//! let mut dec = deckcraft_hevc::Decoder::new();
|
||||
//! let stream = std::fs::read("video.h265").unwrap();
|
||||
//! for pic in dec.decode(&stream, 0).unwrap() {
|
||||
//! println!("{}x{} poc {}", pic.width, pic.height, pic.poc);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
use crate::error::{Result, ensure, unsupported};
|
||||
use crate::spec_tables::{DEFAULT_SCALING_INTER, DEFAULT_SCALING_INTRA};
|
||||
use crate::tables::scan_diag;
|
||||
use slidecraft_bitstream::BitReader;
|
||||
use deckcraft_bitstream::BitReader;
|
||||
|
||||
/// General profile / tier / level information.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::error::{Result, ensure, invalid};
|
||||
use crate::params::{Pps, Sps, StRps};
|
||||
use slidecraft_bitstream::BitReader;
|
||||
use deckcraft_bitstream::BitReader;
|
||||
|
||||
pub mod nal_type {
|
||||
pub const TRAIL_N: u8 = 0;
|
||||
|
|
|
|||