├── .gitmodules ├── distro ├── macos │ ├── PkgInfo │ ├── installer_background.png │ ├── papercraft.iconset │ │ ├── icon_16x16.png │ │ ├── icon_32x32.png │ │ ├── icon_128x128.png │ │ ├── icon_256x256.png │ │ ├── icon_512x512.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256@2x.png │ │ └── icon_32x32@2x.png │ ├── buildiconset │ ├── Info.plist │ └── installer_background.svg ├── .gitignore ├── docker │ ├── apprun │ ├── Dockerfile │ ├── makeappimage │ └── com.rodrigorc.papercraft.appdata.xml ├── papercraft.desktop ├── win32env ├── win64env ├── makewin32 ├── makewin64 ├── papercraft.xml ├── PKGBUILD ├── makelinux └── com.rodrigorc.papercraft.appdata.xml ├── .gitignore ├── src ├── icons.png ├── papercraft.png ├── Karla-Regular.ttf ├── paper │ ├── model │ │ └── import │ │ │ ├── gltf │ │ │ ├── mod.rs │ │ │ └── importer.rs │ │ │ ├── stl │ │ │ ├── mod.rs │ │ │ ├── importer.rs │ │ │ └── data.rs │ │ │ ├── waveobj │ │ │ ├── mod.rs │ │ │ ├── importer.rs │ │ │ └── data.rs │ │ │ ├── pepakura │ │ │ ├── mod.rs │ │ │ ├── importer.rs │ │ │ └── data.rs │ │ │ └── mod.rs │ ├── mod.rs │ └── craft │ │ └── update.rs ├── shaders │ ├── text.glsl │ ├── paper_line.glsl │ ├── quad.glsl │ ├── paper_solid.glsl │ ├── scene_line.glsl │ └── scene_solid.glsl ├── pdf_metrics.rs ├── config.rs ├── util_gl.rs └── util_3d.rs ├── examples ├── die.craft ├── pikachu.pdf ├── pikachu.craft ├── BunnyLamp.craft ├── bulbasaur.craft └── charmander.craft ├── res ├── papercraft.ico ├── manifest.xml └── resource.rc ├── .github └── workflows │ ├── release.yml │ ├── manual_release.yml │ ├── build_win32native.yml │ ├── build_win64native.yml │ ├── build_release.yml │ ├── build_win32.yml │ ├── build_win64.yml │ ├── build_linux.yml │ └── build_macos.yml ├── thirdparty └── afm │ ├── readme.txt │ └── names.txt ├── Cargo.toml ├── papercraft.svg ├── icons.svg ├── locales ├── messages.pot └── es.po └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /distro/macos/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *~ 3 | -------------------------------------------------------------------------------- /src/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/src/icons.png -------------------------------------------------------------------------------- /examples/die.craft: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/examples/die.craft -------------------------------------------------------------------------------- /res/papercraft.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/res/papercraft.ico -------------------------------------------------------------------------------- /src/papercraft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/src/papercraft.png -------------------------------------------------------------------------------- /examples/pikachu.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/examples/pikachu.pdf -------------------------------------------------------------------------------- /src/Karla-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/src/Karla-Regular.ttf -------------------------------------------------------------------------------- /examples/pikachu.craft: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/examples/pikachu.craft -------------------------------------------------------------------------------- /examples/BunnyLamp.craft: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/examples/BunnyLamp.craft -------------------------------------------------------------------------------- /examples/bulbasaur.craft: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/examples/bulbasaur.craft -------------------------------------------------------------------------------- /examples/charmander.craft: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/examples/charmander.craft -------------------------------------------------------------------------------- /src/paper/model/import/gltf/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | mod importer; 3 | 4 | pub use importer::GltfImporter; 5 | -------------------------------------------------------------------------------- /src/paper/model/import/stl/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | mod importer; 3 | 4 | pub use importer::StlImporter; 5 | -------------------------------------------------------------------------------- /src/paper/model/import/waveobj/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | mod importer; 3 | 4 | pub use importer::WaveObjImporter; 5 | -------------------------------------------------------------------------------- /src/paper/model/import/pepakura/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | mod importer; 3 | 4 | pub use importer::PepakuraImporter; 5 | -------------------------------------------------------------------------------- /distro/macos/installer_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/installer_background.png -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_16x16.png -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_32x32.png -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_128x128.png -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_256x256.png -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_512x512.png -------------------------------------------------------------------------------- /distro/.gitignore: -------------------------------------------------------------------------------- 1 | /AppDir 2 | /win32target 3 | /win64target 4 | /win32inst 5 | /win64inst 6 | Papercraft-*.zip 7 | Papercraft-*.AppImage 8 | opengl_win 9 | -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /distro/macos/papercraft.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigorc/papercraft/HEAD/distro/macos/papercraft.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /distro/docker/apprun: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | HERE="$(dirname "$(readlink -f "${0}")")" 4 | 5 | export LD_LIBRARY_PATH="$HERE/usr/lib/" 6 | exec "$HERE/usr/bin/papercraft" "$@" 7 | -------------------------------------------------------------------------------- /distro/papercraft.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Papercraft 3 | Icon=papercraft 4 | Exec=papercraft %F 5 | Terminal=false 6 | Type=Application 7 | MimeType=application/x-papercraft 8 | Categories=Graphics;3DGraphics; 9 | Keywords=3D;Papercraft;craft 10 | StartupNotify=false 11 | -------------------------------------------------------------------------------- /distro/win32env: -------------------------------------------------------------------------------- 1 | export PKG_CONFIG_ALLOW_CROSS="1" 2 | export PKG_CONFIG_LIBDIR="/usr/i686-w64-mingw32/lib/pkgconfig/" 3 | export CARGO_TARGET_DIR=$(pwd)/win32target 4 | export WINEPREFIX=$HOME/.wine-xxx 5 | export WINDRES=i686-w64-mingw32-windres 6 | export RUSTFLAGS="-Clink-arg=-mwindows" 7 | export PS1="W $PS1" 8 | -------------------------------------------------------------------------------- /distro/win64env: -------------------------------------------------------------------------------- 1 | export PKG_CONFIG_ALLOW_CROSS="1" 2 | export PKG_CONFIG_LIBDIR="/usr/x86_64-w64-mingw32/lib/pkgconfig/" 3 | export CARGO_TARGET_DIR=$(pwd)/win64target 4 | export WINEPREFIX=$HOME/.wine-xxx 5 | export WINDRES=x86_64-w64-mingw32-windres 6 | export RUSTFLAGS="-Clink-arg=-mwindows" 7 | export PS1="W64 $PS1" 8 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build_release: 10 | permissions: 11 | contents: write 12 | name: BuildRelease 13 | uses: ./.github/workflows/build_release.yml 14 | with: 15 | tag_name: ${{ github.ref_name }} 16 | 17 | -------------------------------------------------------------------------------- /distro/makewin32: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | VERSION="$(git describe --tag)" 4 | source ./win32env 5 | cargo build --release --target i686-pc-windows-gnu 6 | rm -rf pkg 7 | mkdir -p pkg 8 | cd pkg 9 | ln -s ../win32inst papercraft 10 | rm -f "../Papercraft-$VERSION-win32.zip" 11 | zip -r "../Papercraft-$VERSION-win32.zip" papercraft 12 | cd .. 13 | rm -r pkg 14 | -------------------------------------------------------------------------------- /distro/makewin64: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | VERSION="$(git describe --tag)" 4 | source ./win64env 5 | cargo build --release --target x86_64-pc-windows-gnu 6 | rm -rf pkg 7 | mkdir -p pkg 8 | cd pkg 9 | ln -s ../win64inst papercraft 10 | rm -f "../Papercraft-$VERSION-win64.zip" 11 | zip -r "../Papercraft-$VERSION-win64.zip" papercraft 12 | cd .. 13 | rm -r pkg 14 | -------------------------------------------------------------------------------- /distro/papercraft.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Papercraft model 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/manual_release.yml: -------------------------------------------------------------------------------- 1 | name: ManualRelease 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag_name: 7 | type: string 8 | required: true 9 | 10 | jobs: 11 | build_release: 12 | permissions: 13 | contents: write 14 | name: BuildRelease 15 | uses: ./.github/workflows/build_release.yml 16 | with: 17 | tag_name: ${{ inputs.tag_name }} 18 | -------------------------------------------------------------------------------- /res/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shaders/text.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | uniform mat3 m; 4 | 5 | in vec2 pos; 6 | in vec2 uv; 7 | 8 | out vec2 v_uv; 9 | 10 | void main(void) { 11 | gl_Position = vec4((m * vec3(pos, 1.0)).xy, 0.0, 1.0); 12 | v_uv = uv; 13 | } 14 | 15 | ### 16 | 17 | #version 140 18 | 19 | uniform sampler2D tex; 20 | 21 | in vec2 v_uv; 22 | out vec4 out_frag_color; 23 | 24 | void main(void) { 25 | vec4 c = texture(tex, v_uv); 26 | out_frag_color = vec4(0.0, 0.0, 0.0, c.a); 27 | } 28 | -------------------------------------------------------------------------------- /thirdparty/afm/readme.txt: -------------------------------------------------------------------------------- 1 | This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, 2 | and distributed for any purpose and without charge, with or without modification, 3 | provided that all copyright notices are retained; that the AFM files are not 4 | distributed without this file; that all modifications to this file or any of 5 | the AFM files are prominently noted in the modified file(s); and that this 6 | paragraph is not modified. Adobe Systems has no responsibility or obligation 7 | to support the use of the AFM files. 8 | -------------------------------------------------------------------------------- /distro/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | WORKDIR /app 3 | RUN ln -sf /usr/share/zoneinfo/Europe/Madrid /etc/localtime 4 | RUN apt -y update 5 | RUN apt -y upgrade 6 | RUN apt -y install curl gcc pkg-config xz-utils fuse libclang-dev file libfreetype6-dev 7 | RUN apt -y install g++ 8 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.rs 9 | # Rust 1.76 10 | RUN sh rustup.rs -y --profile minimal -c rustfmt 11 | copy g++ /usr/local/bin/g++ 12 | copy g++ /usr/local/bin/c++ 13 | COPY linuxdeploy ./ 14 | COPY makeappimage ./ 15 | COPY apprun ./ 16 | -------------------------------------------------------------------------------- /src/shaders/paper_line.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | uniform mat3 m; 4 | 5 | in vec2 pos_2d; 6 | in vec4 color; 7 | in float line_dash; 8 | 9 | out float v_line_dash; 10 | out vec4 v_color; 11 | 12 | void main(void) { 13 | v_line_dash = line_dash; 14 | v_color = color; 15 | gl_Position = vec4((m * vec3(pos_2d, 1.0)).xy, 0.0, 1.0); 16 | } 17 | 18 | ### 19 | 20 | #version 140 21 | 22 | in vec4 v_color; 23 | in float v_line_dash; 24 | out vec4 out_frag_color; 25 | 26 | void main(void) { 27 | float alpha = 1.0 - step(0.5, mod(v_line_dash, 1.0)); 28 | out_frag_color = vec4(v_color.rgb, v_color.a * alpha); 29 | } 30 | -------------------------------------------------------------------------------- /src/shaders/quad.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | /* 4 | A single triangle can cover the whole viewport: 5 | 3 * 6 | | \ 7 | 2 + + 8 | | \ 9 | 1 +-----+ 10 | | | \ 11 | 0 + + | + 12 | | | \ 13 | -1 *--+--+--+--* 14 | -1 0 1 2 3 15 | */ 16 | 17 | vec2 positions[3] = vec2[]( 18 | vec2(-1.0, -1.0), 19 | vec2(3.0, -1.0), 20 | vec2(-1.0, 3.0) 21 | ); 22 | 23 | void main(void) { 24 | gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0); 25 | } 26 | 27 | ### 28 | 29 | #version 140 30 | 31 | uniform vec4 color; 32 | out vec4 out_frag_color; 33 | 34 | void main(void) { 35 | out_frag_color = color; 36 | } 37 | -------------------------------------------------------------------------------- /distro/macos/buildiconset: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Silly icns format... 4 | 5 | rm -r papercraft.iconset 6 | mkdir papercraft.iconset 7 | cd papercraft.iconset 8 | 9 | for s in 16 32 64 128 256 512 10 | do 11 | SZ=${s}x${s} 12 | gm convert -background None ../../../papercraft.svg -resize $SZ -gravity center -extent $SZ icon_$SZ.png 13 | done 14 | 15 | cp icon_32x32.png icon_16x16@2x.png 16 | mv icon_64x64.png icon_32x32@2x.png 17 | cp icon_256x256.png icon_128x128@2x.png 18 | cp icon_512x512.png icon_256x256@2x.png 19 | 20 | # Now run "iconutil -c icns papercraft.iconset" to build the icon 21 | # 22 | # For the backgorund 23 | # gm convert -units PixelsPerInch installer_background_.png -density 72 installer_background.png 24 | -------------------------------------------------------------------------------- /src/shaders/paper_solid.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | uniform mat3 m; 4 | 5 | in vec2 pos_2d; 6 | in vec2 uv; 7 | in float mat; 8 | in vec4 color; 9 | 10 | out vec3 v_uv; 11 | out vec4 v_color; 12 | 13 | void main(void) { 14 | gl_Position = vec4((m * vec3(pos_2d, 1.0)).xy, 0.0, 1.0); 15 | v_uv = vec3(uv, mat); 16 | v_color = color; 17 | } 18 | 19 | ### 20 | 21 | #version 140 22 | 23 | uniform bool texturize; 24 | uniform vec4 notex_color; 25 | uniform sampler2DArray tex; 26 | 27 | in vec3 v_uv; 28 | in vec4 v_color; 29 | out vec4 out_frag_color; 30 | 31 | void main(void) { 32 | vec4 c; 33 | if (texturize) { 34 | c = texture(tex, v_uv); 35 | } else { 36 | c = notex_color; 37 | } 38 | out_frag_color = mix(c, vec4(v_color.rgb, 1.0), v_color.a); 39 | } 40 | -------------------------------------------------------------------------------- /res/resource.rc: -------------------------------------------------------------------------------- 1 | #pragma code_page(65001) 2 | #include 3 | #include "papercraft.h" 4 | 5 | 1 VERSIONINFO 6 | FILEVERSION 1, 0, 0, 0 7 | PRODUCTVERSION 1, 0, 0, 0 8 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 9 | FILEFLAGS 0x0 10 | FILEOS VOS_NT_WINDOWS32 11 | FILETYPE VFT_APP 12 | FILESUBTYPE VFT2_UNKNOWN 13 | { 14 | BLOCK "StringFileInfo" 15 | { 16 | BLOCK "040904B0" 17 | { 18 | VALUE "LegalCopyright", PC_REPO 19 | VALUE "FileDescription", PC_PROJECT 20 | VALUE "FileVersion", PC_VERSION 21 | VALUE "ProductVersion", PC_VERSION 22 | VALUE "ProductName", PC_PROJECT 23 | } 24 | } 25 | BLOCK "VarFileInfo" 26 | { 27 | VALUE "Translation", 0x409, 0x4B0 28 | } 29 | } 30 | 31 | 1 ICON "res/papercraft.ico" 32 | 1 MANIFEST "res/manifest.xml" 33 | -------------------------------------------------------------------------------- /distro/docker/makeappimage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | mkdir papercraft 5 | cd papercraft 6 | tar -xf ../source.tar 7 | source $HOME/.cargo/env 8 | BINDGEN_EXTRA_CLANG_ARGS=-I/usr/lib/gcc/x86_64-linux-gnu/7/include cargo build --release 9 | cd .. 10 | 11 | export VERSION="$1" 12 | export ARCH=x86_64 13 | rm -rf AppDir 14 | ./linuxdeploy --appdir=AppDir 15 | mkdir -p AppDir/usr/share/metainfo/ 16 | cp papercraft/distro/papercraft.desktop AppDir/usr/share/applications/ 17 | cp papercraft/distro/com.rodrigorc.papercraft.appdata.xml AppDir/usr/share/metainfo/ 18 | cp papercraft/target/release/papercraft AppDir/usr/bin/ 19 | cp papercraft/src/papercraft.png AppDir/usr/share/icons/hicolor/128x128/apps/ 20 | ./linuxdeploy \ 21 | --appdir=AppDir \ 22 | --desktop-file=AppDir/usr/share/applications/papercraft.desktop \ 23 | --output appimage \ 24 | --exclude-library="libglib-2.0.*" \ 25 | --custom-apprun=apprun 26 | -------------------------------------------------------------------------------- /distro/macos/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleGetInfoString 6 | Papercraft 7 | CFBundleExecutable 8 | Papercraft 9 | CFBundleIdentifier 10 | com.rodrigorc.papercraft 11 | CFBundleName 12 | Papercraft 13 | CFBundleIconFile 14 | Papercraft.icns 15 | CFBundleShortVersionString 16 | 1.00 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundlePackageType 20 | APPL 21 | IFMajorVersion 22 | 0 23 | IFMinorVersion 24 | 1 25 | 26 | 27 | -------------------------------------------------------------------------------- /distro/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Rodrigo Rivas Costa 2 | pkgname=papercraft 3 | pkgver=2.4 4 | pkgrel=1 5 | pkgdesc="A tool to unwrap 3D paper models" 6 | arch=('i686' 'x86_64') 7 | url="http://github.com/rodrigorc/papercraft" 8 | license=('GPL3') 9 | depends=('bzip2' 'shared-mime-info') 10 | makedepends=('git' 'rust') 11 | _commit=v$pkgver 12 | source=("${pkgname}::git+file://$PWD/..#commit=$_commit") 13 | sha512sums=('SKIP') 14 | 15 | build() { 16 | cd ${srcdir}/${pkgname} 17 | git submodule update --init 18 | cargo build --release 19 | } 20 | 21 | package() { 22 | cd ${srcdir}/${pkgname} 23 | install -Dm755 target/release/papercraft ${pkgdir}/usr/bin/papercraft 24 | install -Dm644 src/papercraft.png ${pkgdir}/usr/share/icons/hicolor/192x192/apps/papercraft.png 25 | install -Dm644 distro/papercraft.desktop ${pkgdir}/usr/share/applications/papercraft.desktop 26 | install -Dm644 distro/papercraft.xml ${pkgdir}/usr/share/mime/packages/papercraft.xml 27 | } 28 | -------------------------------------------------------------------------------- /distro/makelinux: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd docker 5 | 6 | # There is no linuxdeploy here, you have to get it from elsewhere, in this case from the path 7 | cp "$(which linuxdeploy)" . 8 | docker buildx build --tag papercraft . 9 | rm linuxdeploy 10 | 11 | COMMIT="${1:-HEAD}" 12 | VERSION="$(git describe --tag "$COMMIT")" 13 | echo Building $COMMIT as $VERSION 14 | HASH="$(git rev-parse $COMMIT)" 15 | 16 | # Send a copy of HEAD to the Docker container 17 | # git-archive does not work with submodules 18 | #(cd $(git rev-parse --show-toplevel); git archive --format=tar "$COMMIT") > source.tar 19 | #git clone --recurse-submodules "$(git rev-parse --show-toplevel)" clone 20 | rm -rf clone 21 | rm -f ../source.tar 22 | git clone -n "$(git rev-parse --show-toplevel)" clone 23 | cd clone 24 | git checkout $HASH 25 | git submodule update --init 26 | rm -rf .git 27 | tar cf ../source.tar * 28 | cd .. 29 | rm -rf clone 30 | 31 | docker run -it -v $(realpath source.tar):/app/source.tar --device /dev/fuse --cap-add SYS_ADMIN papercraft ./makeappimage "$VERSION" 32 | 33 | CNT="$(docker ps -lq)" 34 | docker cp "$CNT:/app/Papercraft-$VERSION-x86_64.AppImage" .. 35 | 36 | rm source.tar 37 | -------------------------------------------------------------------------------- /src/pdf_metrics.rs: -------------------------------------------------------------------------------- 1 | // The lopdf crate does not have font metrics, that is a viewer thing... 2 | // That said there is a bunch of well known files *.afm, that contain the metrics of each standard PDF font. 3 | // I have written a quick'n'dirty `build.rs` parser that reads the *.afm file and converts it into 4 | // a few static Rust structures. 5 | 6 | mod helvetica { 7 | include!(concat!(env!("OUT_DIR"), "/helvetica_afm.rs")); 8 | } 9 | 10 | fn find_in_vec_tuple(key: char, data: &[(char, V)]) -> Option<&V> { 11 | let i = data.binary_search_by_key(&key, |(a, _)| *a).ok()?; 12 | Some(&data[i].1) 13 | } 14 | 15 | /// Given a text returns the total width and a list of (kerning, glyph-id). 16 | pub fn measure_helvetica(text: &str) -> (i32, Vec<(i64, u16)>) { 17 | let mut width = 0; 18 | let mut prev = '\u{0}'; 19 | let mut cps = Vec::with_capacity(text.len()); 20 | for c in text.chars() { 21 | let Some(info) = find_in_vec_tuple(c, &helvetica::CHARS) else { 22 | continue; 23 | }; 24 | let kern = find_in_vec_tuple(prev, info.kerns).copied().unwrap_or(0); 25 | 26 | width += info.width as i32 + kern; 27 | cps.push((-kern as i64, c as u16)); 28 | prev = c; 29 | } 30 | (width, cps) 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build_win32native.yml: -------------------------------------------------------------------------------- 1 | name: BuildWin32Native 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | name: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | papercraft: 12 | name: ${{ inputs.name }} 13 | runs-on: windows-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | submodules: 'recursive' 19 | - name: VS 20 | uses: ilammy/msvc-dev-cmd@v1 21 | - name: Build Win32 22 | run: | 23 | $env:RC="rc.exe" 24 | cargo build --release --target=i686-pc-windows-msvc 25 | - name: Pkg win32 26 | run: | 27 | mkdir pkg32/papercraft 28 | cd pkg32/papercraft 29 | copy ../../target/i686-pc-windows-msvc/release/papercraft.exe . 30 | mkdir examples 31 | cd examples 32 | copy ../../../examples/*.craft . 33 | cd ../.. 34 | 7z a "../Papercraft-${{ inputs.name }}-win32.zip" papercraft 35 | - name: Upload artifact win32 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: Papercraft-${{ inputs.name }}-win32.zip 39 | path: Papercraft-${{ inputs.name }}-win32.zip 40 | if-no-files-found: error 41 | -------------------------------------------------------------------------------- /.github/workflows/build_win64native.yml: -------------------------------------------------------------------------------- 1 | name: BuildWin64Native 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | name: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | papercraft: 12 | name: ${{ inputs.name }} 13 | runs-on: windows-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | submodules: 'recursive' 19 | - name: VS 20 | uses: ilammy/msvc-dev-cmd@v1 21 | - name: Build Win64 22 | run: | 23 | $env:RC="rc.exe" 24 | cargo build --release --target=x86_64-pc-windows-msvc 25 | - name: Pkg win64 26 | run: | 27 | mkdir pkg64/papercraft 28 | cd pkg64/papercraft 29 | copy ../../target/x86_64-pc-windows-msvc/release/papercraft.exe . 30 | mkdir examples 31 | cd examples 32 | copy ../../../examples/*.craft . 33 | cd ../.. 34 | 7z a "../Papercraft-${{ inputs.name }}-win64.zip" papercraft 35 | - name: Upload artifact win64 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: Papercraft-${{ inputs.name }}-win64.zip 39 | path: Papercraft-${{ inputs.name }}-win64.zip 40 | if-no-files-found: error 41 | -------------------------------------------------------------------------------- /src/shaders/scene_line.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | uniform mat4 m; 4 | // half view size, actually 5 | uniform vec2 view_size; 6 | 7 | in vec3 pos_3d; 8 | in vec3 pos_b; 9 | in vec4 color; 10 | // half thickness, actually 11 | in float thick; 12 | in int top; 13 | 14 | out vec4 v_color; 15 | 16 | void main(void) { 17 | v_color = color; 18 | vec4 va = m * vec4(pos_3d, 1.0); 19 | vec4 vb = m * vec4(pos_b, 1.0); 20 | va.xy = (va.xy / va.w + vec2(1.0)) * view_size; 21 | va.z = (va.z - 0.01) / va.w; 22 | vb.xy = (vb.xy / vb.w + vec2(1.0)) * view_size; 23 | vb.z = (vb.z - 0.01) / vb.w; 24 | 25 | vec2 vline = normalize(vb.xy - va.xy); 26 | vec2 nvline = vec2(-vline.y, vline.x) * thick; 27 | 28 | va.xy = (va.xy + nvline) / view_size - vec2(1.0); 29 | 30 | va.xyz *= va.w; 31 | gl_Position = va; 32 | if (top == 0) 33 | { 34 | gl_Position.z = gl_Position.z * 0.8 + 0.1 * gl_Position.w; 35 | } else if (top > 0) { 36 | gl_Position.z = gl_Position.z * 0.1; 37 | } else { 38 | gl_Position.z = gl_Position.z * 0.1 + 0.9 * gl_Position.w; 39 | v_color.rgb = mix(vec3(0.2, 0.2, 0.4), v_color.rgb, 0.25); 40 | } 41 | } 42 | 43 | ### 44 | 45 | #version 140 46 | 47 | in vec4 v_color; 48 | out vec4 out_frag_color; 49 | 50 | void main(void) { 51 | out_frag_color = v_color; 52 | } 53 | -------------------------------------------------------------------------------- /distro/com.rodrigorc.papercraft.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.rodrigorc.papercraft 5 | FSFAP 6 | GPL-3.0-or-later 7 | Papercraft 8 | Tool to unwrap paper 3D models to build them in paper 9 | 10 | 11 |

12 | Papercraft is a tool to unwrap paper 3D models, so that you can cut and glue them together and get a real world paper model. 13 |

14 |
15 | 16 | papercraft.desktop 17 | 18 | 19 | 20 | Main UI 21 | https://user-images.githubusercontent.com/1128630/168819283-d1918ef0-6298-4230-b25c-64d02a021dce.png 22 | 23 | 24 | Dialog options 25 | https://user-images.githubusercontent.com/1128630/170358231-37c8d240-bc70-4d68-af88-53dfba44f361.png 26 | 27 | 28 | 29 | https://github.com/rodrigorc/papercraft 30 | 31 | 32 | 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /distro/docker/com.rodrigorc.papercraft.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.rodrigorc.papercraft 5 | FSFAP 6 | GPL-3.0-or-later 7 | Papercraft 8 | Tool to unwrap paper 3D models to build them in paper 9 | 10 | 11 |

12 | Papercraft is a tool to unwrap paper 3D models, so that you can cut and glue them together and get a real world paper model. 13 |

14 |
15 | 16 | papercraft.desktop 17 | 18 | 19 | 20 | Main UI 21 | https://user-images.githubusercontent.com/1128630/168819283-d1918ef0-6298-4230-b25c-64d02a021dce.png 22 | 23 | 24 | Dialog options 25 | https://user-images.githubusercontent.com/1128630/170358231-37c8d240-bc70-4d68-af88-53dfba44f361.png 26 | 27 | 28 | 29 | https://github.com/rodrigorc/papercraft 30 | 31 | 32 | 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde::*; 3 | use std::path::PathBuf; 4 | 5 | #[derive(Serialize, Deserialize, Debug, Clone)] 6 | pub struct Config { 7 | pub locale: String, 8 | pub light_mode: bool, 9 | } 10 | 11 | impl Config { 12 | fn file_name() -> Result { 13 | let dirs = directories::ProjectDirs::from("com", "rodrigorc", "papercraft") 14 | .ok_or(anyhow::anyhow!("Unknown configuration directory"))?; 15 | let dir = dirs.preference_dir(); 16 | Ok(PathBuf::from(dir).join("papercraft.json")) 17 | } 18 | fn load() -> Result { 19 | let file_name = Self::file_name()?; 20 | let f = std::fs::File::open(file_name)?; 21 | let f = std::io::BufReader::new(f); 22 | let cfg = serde_json::from_reader(f)?; 23 | Ok(cfg) 24 | } 25 | pub fn save(&self) -> Result<()> { 26 | let file_name = Self::file_name()?; 27 | if let Some(d) = file_name.parent() { 28 | std::fs::create_dir_all(d)? 29 | } 30 | let f = std::fs::File::create(file_name)?; 31 | let f = std::io::BufWriter::new(f); 32 | serde_json::to_writer(f, self)?; 33 | Ok(()) 34 | } 35 | 36 | pub fn load_or_default() -> Config { 37 | if let Ok(c) = Self::load() { 38 | return c; 39 | } 40 | let locale = sys_locale::get_locale().unwrap_or(String::from("en")); 41 | let locale = locale.split('-').next().unwrap().to_owned(); 42 | Config { 43 | locale, 44 | light_mode: false, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/build_release.yml: -------------------------------------------------------------------------------- 1 | name: BuildRelease 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | tag_name: 7 | type: string 8 | required: true 9 | 10 | jobs: 11 | build_linux: 12 | name: BuildLinux 13 | uses: ./.github/workflows/build_linux.yml 14 | with: 15 | name: ${{ inputs.tag_name }} 16 | build_win32: 17 | name: BuildWin32 18 | uses: ./.github/workflows/build_win32.yml 19 | with: 20 | name: ${{ inputs.tag_name }} 21 | build_win64: 22 | name: BuildWin64 23 | uses: ./.github/workflows/build_win64.yml 24 | with: 25 | name: ${{ inputs.tag_name }} 26 | build_macos: 27 | name: BuildMacOS 28 | uses: ./.github/workflows/build_macos.yml 29 | with: 30 | name: ${{ inputs.tag_name }} 31 | release: 32 | name: Release 33 | needs: [build_linux, build_win32, build_win64, build_macos] 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Download Linux 37 | uses: actions/download-artifact@v4 38 | with: 39 | name: Papercraft-${{ inputs.tag_name }}-x86_64.AppImage 40 | - name: Download Win32 41 | uses: actions/download-artifact@v4 42 | with: 43 | name: Papercraft-${{ inputs.tag_name }}-win32.zip 44 | - name: Download Win64 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: Papercraft-${{ inputs.tag_name }}-win64.zip 48 | - name: Download MacOS 49 | uses: actions/download-artifact@v4 50 | with: 51 | name: Papercraft-${{ inputs.tag_name }}-MacOS.dmg 52 | - name: Upload all to release 53 | uses: svenstaro/upload-release-action@2.9.0 54 | with: 55 | tag: ${{ inputs.tag_name }} 56 | prerelease: true 57 | file_glob: true 58 | file: Papercraft-${{ inputs.tag_name }}-* 59 | 60 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "papercraft" 3 | version = "2.10.0" 4 | authors = ["Rodrigo Rivas Costa "] 5 | edition = "2024" 6 | license = "GPL-3.0-or-later" 7 | repository = "https://github.com/rodrigorc/papercraft" 8 | description = "Papercraft is a tool to unwrap 3D models" 9 | keywords = ["papercraft", "handcraft"] 10 | 11 | [profile.release] 12 | strip = "symbols" 13 | lto = true 14 | 15 | [features] 16 | # In linux this is basically free, but in Windows this adds a bunch of dependencies 17 | # for little gain. 18 | freetype=["easy-imgui-window/freetype"] 19 | 20 | [profile.dev.package.image] 21 | opt-level = 3 22 | [profile.dev.package.zip] 23 | opt-level = 3 24 | [profile.dev.package.flate2] 25 | opt-level = 3 26 | 27 | [build-dependencies] 28 | include-po = "0.2" 29 | 30 | [dependencies] 31 | cgmath = { version = "0.18", features = ["mint"] } 32 | anyhow = "1" 33 | base64 = "0.22" 34 | slotmap = "1" 35 | serde = { version = "1", features = ["derive"] } 36 | serde_json = "1" 37 | zip = "6.0.0" 38 | bitflags = "2" 39 | image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } 40 | clap = { version = "4", features = ["derive", "cargo"] } 41 | log = "0.4" 42 | env_logger = "0.11" 43 | flate2 = "1" 44 | fxhash = "0.2" 45 | signal-hook = "0.3" 46 | opener = { version = "0.8", default-features = false } 47 | maybe-owned = { version = "0.3", features = ["serde"] } 48 | lazy_static = "1" 49 | tr = { version = "0.1.10", default-features = false } 50 | 51 | lopdf = "0.38" 52 | time = { version = "0.3", features = ["local-offset"] } 53 | 54 | easy-imgui-window = "0.20.0" 55 | easy-imgui-filechooser = { version = "0.3", features = ["tr"] } 56 | 57 | sys-locale = "0.3" 58 | directories = "6" 59 | rayon = "1" 60 | cancel-rw = "0.1" 61 | 62 | reqwest = { version = "0.12", default-features = false, features = ["blocking", "default-tls"] } 63 | 64 | -------------------------------------------------------------------------------- /.github/workflows/build_win32.yml: -------------------------------------------------------------------------------- 1 | name: BuildWin32 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | name: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | papercraft: 12 | name: ${{ inputs.name }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Install pre-requisites 16 | run: | 17 | rustup target add i686-pc-windows-gnu 18 | sudo apt-get install -y gcc-mingw-w64 g++-mingw-w64 libz-mingw-w64-dev 19 | sudo ln -s /usr/lib/gcc/i686-w64-mingw32/10-win32/include/c++ /usr/i686-w64-mingw32/include/c++ 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: 'recursive' 24 | - name: Build Win32 25 | run: | 26 | env PKG_CONFIG_ALLOW_CROSS="1" \ 27 | PKG_CONFIG_LIBDIR="/usr/i686-w64-mingw32/lib/pkgconfig/" \ 28 | WINDRES=i686-w64-mingw32-windres \ 29 | RUSTFLAGS="-Clink-arg=-mwindows" \ 30 | CARGO_TARGET_DIR=win32target \ 31 | cargo build --release --target i686-pc-windows-gnu 32 | - name: Pkg win32 33 | run: | 34 | mkdir -p pkg32/papercraft 35 | cd pkg32/papercraft 36 | ln -s ../../win32target/i686-pc-windows-gnu/release/papercraft.exe . 37 | ln -s /usr/lib/gcc/i686-w64-mingw32/*-win32/libstdc++-6.dll . 38 | ln -s /usr/lib/gcc/i686-w64-mingw32/*-win32/libgcc_s_dw2-1.dll . 39 | mkdir examples 40 | cd examples 41 | ln -s ../../../examples/*.craft . 42 | cd ../.. 43 | zip -r "../Papercraft-${{ inputs.name }}-win32.zip" papercraft 44 | - name: Upload artifact win32 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: Papercraft-${{ inputs.name }}-win32.zip 48 | path: Papercraft-${{ inputs.name }}-win32.zip 49 | if-no-files-found: error 50 | -------------------------------------------------------------------------------- /.github/workflows/build_win64.yml: -------------------------------------------------------------------------------- 1 | name: BuildWin64 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | name: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | papercraft: 12 | name: ${{ inputs.name }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Install pre-requisites 16 | run: | 17 | rustup target add x86_64-pc-windows-gnu 18 | sudo apt-get install -y gcc-mingw-w64 g++-mingw-w64 libz-mingw-w64-dev 19 | sudo ln -s /usr/lib/gcc/x86_64-w64-mingw32/10-win32/include/c++ /usr/x86_64-w64-mingw32/include/c++ 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: 'recursive' 24 | - name: Build Win64 25 | run: | 26 | env PKG_CONFIG_ALLOW_CROSS="1" \ 27 | PKG_CONFIG_LIBDIR="/usr/x86_64-w64-mingw32/lib/pkgconfig/" \ 28 | WINDRES=x86_64-w64-mingw32-windres \ 29 | RUSTFLAGS="-Clink-arg=-mwindows" \ 30 | CARGO_TARGET_DIR=win64target \ 31 | cargo build --release --target x86_64-pc-windows-gnu 32 | - name: Pkg win64 33 | run: | 34 | mkdir -p pkg64/papercraft 35 | cd pkg64/papercraft 36 | ln -s ../../win64target/x86_64-pc-windows-gnu/release/papercraft.exe . 37 | ln -s /usr/lib/gcc/x86_64-w64-mingw32/*-win32/libstdc++-6.dll . 38 | ln -s /usr/lib/gcc/x86_64-w64-mingw32/*-win32/libgcc_s_seh-1.dll . 39 | mkdir examples 40 | cd examples 41 | ln -s ../../../examples/*.craft . 42 | cd ../.. 43 | zip -r "../Papercraft-${{ inputs.name }}-win64.zip" papercraft 44 | - name: Upload artifact win64 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: Papercraft-${{ inputs.name }}-win64.zip 48 | path: Papercraft-${{ inputs.name }}-win64.zip 49 | if-no-files-found: error 50 | -------------------------------------------------------------------------------- /src/shaders/scene_solid.glsl: -------------------------------------------------------------------------------- 1 | #version 140 2 | 3 | uniform mat4 m; 4 | uniform mat3 mnormal; 5 | uniform vec3 lights[2]; 6 | 7 | in vec3 pos_3d; 8 | in vec3 normal; 9 | in vec2 uv; 10 | in float mat; 11 | in vec4 color; 12 | in int top; 13 | 14 | out vec3 v_uv; 15 | out float v_light; 16 | out vec4 v_color; 17 | out float v_alpha; 18 | 19 | 20 | void main(void) { 21 | gl_Position = m * vec4(pos_3d, 1.0); 22 | vec3 obj_normal = normalize(mnormal * normal); 23 | 24 | float light = 0.2; 25 | for (int i = 0; i < 2; ++i) { 26 | float diffuse = max(abs(dot(obj_normal, -lights[i])), 0.0); 27 | light += diffuse; 28 | } 29 | v_light = light; 30 | v_uv = vec3(uv, mat); 31 | v_color = color; 32 | v_alpha = 1.0; 33 | if (top == 0) 34 | { 35 | gl_Position.z = gl_Position.z * 0.8 + 0.1 * gl_Position.w; 36 | } else if (top > 0) { 37 | gl_Position.z = gl_Position.z * 0.1; 38 | } else { 39 | gl_Position.z = gl_Position.z * 0.1 + 0.9 * gl_Position.w; 40 | v_alpha = 0.25; 41 | } 42 | } 43 | 44 | ### 45 | 46 | #version 140 47 | 48 | uniform bool texturize; 49 | uniform sampler2DArray tex; 50 | 51 | in vec3 v_uv; 52 | in float v_light; 53 | in vec4 v_color; 54 | in float v_alpha; 55 | out vec4 out_frag_color; 56 | 57 | void main(void) { 58 | vec4 base; 59 | 60 | if (gl_FrontFacing) 61 | { 62 | vec4 c; 63 | if (texturize) { 64 | c = texture(tex, v_uv); 65 | } else { 66 | c = vec4(0.75, 0.75, 0.75, 1.0); 67 | } 68 | base = mix(c, vec4(v_color.rgb, 1.0), v_color.a); 69 | } 70 | else 71 | { 72 | base = mix(vec4(0.8, 0.3, 0.3, 1.0), vec4(v_color.rgb, 1.0), v_color.a / 2.0); 73 | } 74 | // do alpha blending with full-white and output a fully opaque fragment, simulating the texture over paper 75 | vec3 color = v_light * mix(vec3(1.0, 1.0, 1.0), base.rgb, base.a); 76 | out_frag_color = vec4(mix(vec3(0.2, 0.2, 0.4), color, v_alpha), 1.0); 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/build_linux.yml: -------------------------------------------------------------------------------- 1 | name: BuildLinux 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | name: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | papercraft: 12 | name: ${{ inputs.name }} 13 | # Oldest LTS ubuntu still with support 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | submodules: 'recursive' 20 | - name: Bin dir 21 | run: | 22 | mkdir bin 23 | echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH 24 | - name: LinuxDeploy 25 | run: | 26 | ( cd bin ; wget -nv https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage ) 27 | chmod a+x bin/linuxdeploy-x86_64.AppImage 28 | - name: Build 29 | run: | 30 | cargo build --release 31 | - name: Pack AppImage 32 | run: | 33 | export LINUXDEPLOY_OUTPUT_VERSION="${{ inputs.name }}" 34 | export ARCH=x86_64 35 | # Create the directory tree 36 | linuxdeploy-x86_64.AppImage --appdir=AppDir 37 | mkdir -p AppDir/usr/share/metainfo/ 38 | cp distro/papercraft.desktop AppDir/usr/share/applications/ 39 | cp distro/com.rodrigorc.papercraft.appdata.xml AppDir/usr/share/metainfo/ 40 | cp target/release/papercraft AppDir/usr/bin/ 41 | cp src/papercraft.png AppDir/usr/share/icons/hicolor/128x128/apps/ 42 | linuxdeploy-x86_64.AppImage \ 43 | --appdir=AppDir \ 44 | --desktop-file=AppDir/usr/share/applications/papercraft.desktop \ 45 | --output appimage \ 46 | --exclude-library="libglib-2.0.*" \ 47 | --custom-apprun=distro/docker/apprun 48 | - name: Upload artifact 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: Papercraft-${{ inputs.name }}-x86_64.AppImage 52 | path: Papercraft-${{ inputs.name }}-x86_64.AppImage 53 | if-no-files-found: error 54 | -------------------------------------------------------------------------------- /src/paper/model/import/stl/importer.rs: -------------------------------------------------------------------------------- 1 | use super::super::*; 2 | use super::data; 3 | use cgmath::Zero; 4 | 5 | pub struct StlImporter { 6 | stl: data::Stl, 7 | } 8 | 9 | impl StlImporter { 10 | pub fn new(f: R) -> Result { 11 | let stl = data::Stl::new(f)?; 12 | 13 | Ok(StlImporter { stl }) 14 | } 15 | } 16 | 17 | impl Importer for StlImporter { 18 | // STL doesn't have vertex identity, we consider them the same if they are bitwise identical 19 | type VertexId = [u32; 3]; 20 | 21 | fn build_vertices(&self) -> (bool, Vec) { 22 | // The VertexIndex is {3*nface, +1, +2} 23 | let mut has_normals = false; 24 | let vxs = self 25 | .stl 26 | .triangles() 27 | .iter() 28 | .flat_map(|tri| { 29 | if !has_normals && tri.normal != Vector3::zero() { 30 | has_normals = true; 31 | } 32 | tri.vertices.iter().map(|v| { 33 | Vertex { 34 | // swizzle the coordinates to make the model look at the front 35 | pos: Vector3::new(v.x, v.z, -v.y), 36 | normal: tri.normal, 37 | uv: Vector2::zero(), 38 | } 39 | }) 40 | }) 41 | .collect(); 42 | (has_normals, vxs) 43 | } 44 | 45 | fn vertex_map(&self, i_v: VertexIndex) -> Self::VertexId { 46 | //self.vx_map[usize::from(i_v)] 47 | let i_v = usize::from(i_v); 48 | let v: [f32; 3] = self.stl.triangles()[i_v / 3].vertices[i_v % 3].into(); 49 | v.map(|f| f.to_bits()) 50 | } 51 | 52 | fn face_count(&self) -> usize { 53 | self.stl.triangles().len() 54 | } 55 | 56 | fn faces(&self) -> impl Iterator, MaterialIndex)> { 57 | (0..self.stl.triangles().len() as u32).map(|i_face| { 58 | let i_v0 = 3 * i_face; 59 | ( 60 | [ 61 | VertexIndex(i_v0), 62 | VertexIndex(i_v0 + 1), 63 | VertexIndex(i_v0 + 2), 64 | ], 65 | MaterialIndex(0), 66 | ) 67 | }) 68 | } 69 | 70 | fn build_textures(&self) -> Vec { 71 | vec![Texture::default()] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/build_macos.yml: -------------------------------------------------------------------------------- 1 | name: BuildMacOS 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | name: 7 | type: string 8 | required: true 9 | workflow_dispatch: 10 | inputs: 11 | name: 12 | type: string 13 | required: true 14 | 15 | jobs: 16 | papercraft: 17 | name: ${{ inputs.name }} 18 | runs-on: macos-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: 'recursive' 24 | - name: Rustup 25 | run: | 26 | rustup target add x86_64-apple-darwin 27 | - name: Create DMG 28 | run: | 29 | brew install create-dmg 30 | - name: Build x86 31 | run: | 32 | cargo build --release --target=x86_64-apple-darwin 33 | - name: Build Arm86 34 | run: | 35 | cargo build --release --target=aarch64-apple-darwin 36 | - name: Build DMG 37 | run: | 38 | mkdir -p app_folder/Papercraft.app/Contents/MacOS 39 | mkdir -p app_folder/Papercraft.app/Contents/Resources 40 | cd distro/macos 41 | iconutil -c icns papercraft.iconset 42 | cd ../.. 43 | cp distro/macos/papercraft.icns app_folder/Papercraft.app/Contents/Resources/Papercraft.icns 44 | cp distro/macos/PkgInfo app_folder/Papercraft.app/Contents/ 45 | cp distro/macos/Info.plist app_folder/Papercraft.app/Contents/ 46 | lipo -create target/aarch64-apple-darwin/release/papercraft target/x86_64-apple-darwin/release/papercraft -output \ 47 | app_folder/Papercraft.app/Contents/MacOS/Papercraft 48 | create-dmg \ 49 | --volname "Papercraft" \ 50 | --volicon "distro/macos/papercraft.icns" \ 51 | --background "distro/macos/installer_background.png" \ 52 | --window-pos 200 120 \ 53 | --window-size 800 400 \ 54 | --icon-size 100 \ 55 | --icon "Papercraft.app" 200 200 \ 56 | --app-drop-link 600 200 \ 57 | --hide-extension "Papercraft.app" \ 58 | "Papercraft-${{ inputs.name }}-MacOS.dmg" \ 59 | "app_folder/" 60 | 61 | - name: Upload artifact MacOS 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: Papercraft-${{ inputs.name }}-MacOS.dmg 65 | path: Papercraft-${{ inputs.name }}-MacOS.dmg 66 | if-no-files-found: error 67 | 68 | -------------------------------------------------------------------------------- /src/paper/mod.rs: -------------------------------------------------------------------------------- 1 | mod craft; 2 | mod model; 3 | 4 | pub use craft::*; 5 | pub use model::*; 6 | 7 | use crate::util_3d::*; 8 | use serde::{ 9 | Deserialize, Serialize, 10 | ser::{SerializeSeq, SerializeStruct}, 11 | }; 12 | mod ser { 13 | use super::*; 14 | pub mod vector2 { 15 | use super::*; 16 | pub fn serialize(data: &Vector2, serializer: S) -> Result 17 | where 18 | S: serde::Serializer, 19 | { 20 | let mut seq = serializer.serialize_seq(Some(3))?; 21 | seq.serialize_element(&data.x)?; 22 | seq.serialize_element(&data.y)?; 23 | seq.end() 24 | } 25 | pub fn deserialize<'de, D>(deserializer: D) -> Result 26 | where 27 | D: serde::Deserializer<'de>, 28 | { 29 | let data = <[f32; 2]>::deserialize(deserializer)?; 30 | Ok(Vector2::from(data)) 31 | } 32 | } 33 | pub mod vector3 { 34 | use super::*; 35 | pub fn serialize(data: &Vector3, serializer: S) -> Result 36 | where 37 | S: serde::Serializer, 38 | { 39 | let mut seq = serializer.serialize_seq(Some(3))?; 40 | seq.serialize_element(&data.x)?; 41 | seq.serialize_element(&data.y)?; 42 | seq.serialize_element(&data.z)?; 43 | seq.end() 44 | } 45 | pub fn deserialize<'de, D>(deserializer: D) -> Result 46 | where 47 | D: serde::Deserializer<'de>, 48 | { 49 | let data = <[f32; 3]>::deserialize(deserializer)?; 50 | Ok(Vector3::from(data)) 51 | } 52 | } 53 | // Beware! This serializes pnly the values, not the keys. 54 | pub mod slot_map { 55 | use super::*; 56 | pub fn serialize( 57 | data: &slotmap::SlotMap, 58 | serializer: S, 59 | ) -> Result 60 | where 61 | S: serde::Serializer, 62 | K: slotmap::Key, 63 | V: Serialize, 64 | { 65 | let mut seq = serializer.serialize_seq(Some(data.len()))?; 66 | for (_, d) in data { 67 | seq.serialize_element(d)?; 68 | } 69 | seq.end() 70 | } 71 | pub fn deserialize<'de, D, K, V>( 72 | deserializer: D, 73 | ) -> Result, D::Error> 74 | where 75 | D: serde::Deserializer<'de>, 76 | K: slotmap::Key, 77 | V: Deserialize<'de>, 78 | { 79 | let data = >::deserialize(deserializer)?; 80 | let mut map = slotmap::SlotMap::with_key(); 81 | for d in data { 82 | map.insert(d); 83 | } 84 | Ok(map) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/paper/model/import/gltf/importer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use image::DynamicImage; 3 | use std::cell::Cell; 4 | use std::io::BufRead; 5 | 6 | use super::super::*; 7 | use super::data::*; 8 | 9 | pub struct GltfImporter { 10 | // Cell to avoid cloning 11 | images: Cell>, 12 | // 3 vertices per face 13 | vertices: Vec, 14 | // 1 tex_id per face 15 | textures: Vec, 16 | has_normals: bool, 17 | } 18 | 19 | impl GltfImporter { 20 | pub fn new(mut f: R, file_name: &Path) -> Result { 21 | let mut data = Vec::new(); 22 | f.read_to_end(&mut data)?; 23 | 24 | let gltf = Gltf::parse(&data, file_name)?; 25 | Self::new_inner(gltf) 26 | } 27 | 28 | fn new_inner(gltf: Gltf) -> Result { 29 | let images = gltf.load_images()?; 30 | let mut vertices = Vec::new(); 31 | let mut textures = Vec::new(); 32 | let mut has_normals = true; 33 | gltf.process_scene(|tex, vs, ns, uv| { 34 | for i in 0..3 { 35 | vertices.push(Vertex { 36 | pos: vs[i], 37 | normal: ns.map(|n| n[i]).unwrap_or_else(|| { 38 | has_normals = false; 39 | Vector3::new(0.0, 0.0, 0.0) 40 | }), 41 | uv: uv[i], 42 | }); 43 | } 44 | // texture 0 is the no-tex 45 | textures.push(tex.map(|t| t + 1).unwrap_or(0)) 46 | })?; 47 | 48 | Ok(GltfImporter { 49 | images: Cell::new(images), 50 | vertices, 51 | has_normals, 52 | textures, 53 | }) 54 | } 55 | } 56 | 57 | impl Importer for GltfImporter { 58 | type VertexId = [u32; 3]; 59 | 60 | fn vertex_map(&self, i_v: VertexIndex) -> Self::VertexId { 61 | let i_v = usize::from(i_v); 62 | let v: [f32; 3] = self.vertices[i_v].pos.into(); 63 | v.map(|f| f.to_bits()) 64 | } 65 | 66 | fn build_vertices(&self) -> (bool, Vec) { 67 | let vs = self.vertices.clone(); 68 | (self.has_normals, vs) 69 | } 70 | 71 | fn face_count(&self) -> usize { 72 | self.vertices.len() / 3 73 | } 74 | 75 | fn faces(&self) -> impl Iterator, MaterialIndex)> + '_ { 76 | (0..self.vertices.len() / 3).map(|i| { 77 | let n = 3 * i as u32; 78 | ( 79 | [VertexIndex(n), VertexIndex(n + 1), VertexIndex(n + 2)], 80 | MaterialIndex(self.textures[i]), 81 | ) 82 | }) 83 | } 84 | 85 | fn build_textures(&self) -> Vec { 86 | let mut texs = vec![Texture::default()]; 87 | for (file_name, pixbuf) in self.images.take() { 88 | texs.push(Texture { 89 | file_name, 90 | pixbuf: Some(pixbuf), 91 | }) 92 | } 93 | texs 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/paper/model/import/stl/data.rs: -------------------------------------------------------------------------------- 1 | use crate::paper::import::*; 2 | use anyhow::bail; 3 | use cgmath::Zero; 4 | 5 | #[derive(Debug)] 6 | pub struct Stl { 7 | tris: Vec, 8 | } 9 | 10 | #[derive(Debug)] 11 | pub struct Triangle { 12 | pub normal: Vector3, 13 | pub vertices: [Vector3; 3], 14 | } 15 | 16 | impl Stl { 17 | pub fn new(mut f: R) -> Result { 18 | let mut hdr = [0; 80]; 19 | f.read_exact(&mut hdr[..5])?; 20 | if &hdr[..5] == b"solid" { 21 | Self::new_text(f) 22 | } else { 23 | f.read_exact(&mut hdr[5..])?; 24 | Self::new_binary(f) 25 | } 26 | } 27 | fn new_binary(mut f: R) -> Result { 28 | let rdr = &mut f; 29 | let n_tris = read_u32(rdr)?; 30 | let mut tris = Vec::with_capacity(n_tris as usize); 31 | for _ in 0..n_tris { 32 | let normal = read_vector3_f32(rdr)?; 33 | let v0 = read_vector3_f32(rdr)?; 34 | let v1 = read_vector3_f32(rdr)?; 35 | let v2 = read_vector3_f32(rdr)?; 36 | let _attr = read_u16(rdr)?; 37 | tris.push(Triangle { 38 | normal, 39 | vertices: [v0, v1, v2], 40 | }) 41 | } 42 | 43 | Ok(Stl { tris }) 44 | } 45 | fn new_text(mut f: R) -> Result { 46 | let mut line = String::new(); 47 | // "{solid} NAME" 48 | f.read_line(&mut line)?; 49 | let mut tris = Vec::new(); 50 | loop { 51 | // "facet normal nx ny nz" 52 | line.clear(); 53 | f.read_line(&mut line)?; 54 | let mut words = line.split_ascii_whitespace(); 55 | let w = words.next(); 56 | match w { 57 | Some("endsolid") => break, 58 | Some("facet") => {} 59 | _ => { 60 | bail!(r#"expected "facet""#); 61 | } 62 | } 63 | let w = words.next(); 64 | if w != Some("normal") { 65 | bail!(r#"expected "normal""#); 66 | } 67 | let Some(nx) = words.next() else { 68 | bail!(r#"expected number"#) 69 | }; 70 | let Some(ny) = words.next() else { 71 | bail!(r#"expected number"#) 72 | }; 73 | let Some(nz) = words.next() else { 74 | bail!(r#"expected number"#) 75 | }; 76 | let normal = Vector3::new(nx.parse()?, ny.parse()?, nz.parse()?); 77 | 78 | // do not bother parsing lines without values, they can only result in error 79 | // "outer loop" 80 | line.clear(); 81 | f.read_line(&mut line)?; 82 | 83 | // 3 * ("vertex x y z") 84 | let mut vertices = [Vector3::zero(); 3]; 85 | for vert in &mut vertices { 86 | line.clear(); 87 | f.read_line(&mut line)?; 88 | let mut words = line.split_ascii_whitespace(); 89 | let w = words.next(); 90 | match w { 91 | Some("vertex") => {} 92 | _ => { 93 | bail!(r#"expected "vertex""#); 94 | } 95 | } 96 | let Some(vx) = words.next() else { 97 | bail!(r#"expected number"#) 98 | }; 99 | let Some(vy) = words.next() else { 100 | bail!(r#"expected number"#) 101 | }; 102 | let Some(vz) = words.next() else { 103 | bail!(r#"expected number"#) 104 | }; 105 | *vert = Vector3::new(vx.parse()?, vy.parse()?, vz.parse()?); 106 | } 107 | 108 | // "end loop" 109 | line.clear(); 110 | f.read_line(&mut line)?; 111 | // "end facet" 112 | line.clear(); 113 | f.read_line(&mut line)?; 114 | tris.push(Triangle { normal, vertices }) 115 | } 116 | Ok(Stl { tris }) 117 | } 118 | pub fn triangles(&self) -> &[Triangle] { 119 | &self.tris 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/util_gl.rs: -------------------------------------------------------------------------------- 1 | use crate::glr::{self, Rgba, UniformField}; 2 | use crate::paper::MaterialIndex; 3 | use crate::util_3d::*; 4 | use anyhow::{Result, anyhow}; 5 | use easy_imgui_window::easy_imgui_renderer::easy_imgui_opengl::{attrib, uniform}; 6 | 7 | ////////////////////////////////////// 8 | // Uniforms and vertices 9 | 10 | uniform! { 11 | pub struct Uniforms3D { 12 | pub lights: [Vector3; 2], 13 | pub m: Matrix4, 14 | pub mnormal: Matrix3, 15 | pub tex: i32, 16 | pub texturize: i32, 17 | pub view_size: Vector2, 18 | } 19 | pub struct Uniforms2D { 20 | pub m: Matrix3, 21 | pub tex: i32, 22 | pub texturize: i32, 23 | pub notex_color: Rgba, 24 | } 25 | pub struct UniformQuad { 26 | pub color: Rgba, 27 | } 28 | } 29 | 30 | attrib! { 31 | #[derive(Copy, Clone, Debug)] 32 | #[repr(C)] 33 | pub struct MVertex3D { 34 | pub pos_3d: Vector3, 35 | pub normal: Vector3, 36 | pub uv: Vector2, 37 | pub mat: MaterialIndex, 38 | } 39 | #[derive(Copy, Clone, Debug)] 40 | #[repr(C)] 41 | pub struct MVertex3DLine { 42 | pub pos_3d: Vector3, 43 | pub pos_b: Vector3, 44 | } 45 | #[derive(Copy, Clone, Debug)] 46 | #[repr(C)] 47 | pub struct MLine3DStatus { 48 | pub thick: f32, 49 | pub color: Rgba, 50 | pub top: i8, 51 | } 52 | #[derive(Copy, Clone, Debug)] 53 | #[repr(C)] 54 | pub struct MVertex2D { 55 | pub pos_2d: Vector2, 56 | } 57 | #[derive(Copy, Clone, Debug)] 58 | #[repr(C)] 59 | pub struct MVertex2DColor { 60 | pub pos_2d: Vector2, 61 | pub uv: Vector2, 62 | pub mat: MaterialIndex, 63 | pub color: Rgba, 64 | } 65 | #[derive(Copy, Clone, Debug)] 66 | #[repr(C)] 67 | pub struct MVertex2DLine { 68 | pub pos_2d: Vector2, 69 | pub color: Rgba, 70 | pub line_dash: f32, 71 | } 72 | #[derive(Copy, Clone, Debug)] 73 | #[repr(C)] 74 | pub struct MStatus { 75 | pub color: Rgba, 76 | pub top: i8, 77 | } 78 | #[derive(Copy, Clone, Debug)] 79 | #[repr(C)] 80 | pub struct MVertexText { 81 | pub pos: Vector2, 82 | pub uv: Vector2, 83 | } 84 | } 85 | 86 | impl Default for MVertex2D { 87 | fn default() -> MVertex2D { 88 | MVertex2D { 89 | pos_2d: Vector2::new(0.0, 0.0), 90 | } 91 | } 92 | } 93 | 94 | pub const MLINE3D_HIDDEN: MLine3DStatus = MLine3DStatus { 95 | color: Rgba::new(0.0, 0.0, 0.0, 0.0), 96 | thick: 0.0, 97 | top: -1, 98 | }; 99 | 100 | pub const MLINE3D_NORMAL: MLine3DStatus = MLine3DStatus { 101 | color: Rgba::new(0.0, 0.0, 0.0, 1.0), 102 | thick: 1.0 / 2.0, 103 | top: -1, 104 | }; 105 | 106 | pub const MLINE3D_RIM: MLine3DStatus = MLine3DStatus { 107 | color: Rgba::new(1.0, 1.0, 0.0, 1.0), 108 | thick: 1.0 / 2.0, 109 | top: -1, 110 | }; 111 | 112 | pub const MLINE3D_RIM_TAB: MLine3DStatus = MLine3DStatus { 113 | color: Rgba::new(0.75, 0.75, 0.0, 1.0), 114 | thick: 5.0 / 2.0, 115 | top: -1, 116 | }; 117 | 118 | pub const MLINE3D_CUT: MLine3DStatus = MLine3DStatus { 119 | color: Rgba::new(1.0, 1.0, 1.0, 1.0), 120 | thick: 3.0 / 2.0, 121 | top: -1, 122 | }; 123 | 124 | pub const MSTATUS_UNSEL: MStatus = MStatus { 125 | color: Rgba::new(0.0, 0.0, 0.0, 0.0), 126 | top: 0, 127 | }; 128 | pub const MSTATUS_SEL: MStatus = MStatus { 129 | color: Rgba::new(0.0, 0.0, 1.0, 0.5), 130 | top: 1, 131 | }; 132 | pub const MSTATUS_HI: MStatus = MStatus { 133 | color: Rgba::new(1.0, 0.0, 0.0, 0.75), 134 | top: 1, 135 | }; 136 | 137 | pub fn program_from_source(gl: &glr::GlContext, shaders: &str) -> Result { 138 | let split = shaders 139 | .find("###") 140 | .ok_or_else(|| anyhow!("shader marker not found"))?; 141 | let vertex = &shaders[..split]; 142 | let frag = &shaders[split..]; 143 | let split_2 = frag 144 | .find('\n') 145 | .ok_or_else(|| anyhow!("shader marker not valid"))?; 146 | 147 | let mut frag = &frag[split_2..]; 148 | 149 | let geom = if let Some(split) = frag.find("###") { 150 | let geom = &frag[split..]; 151 | frag = &frag[..split]; 152 | let split_2 = geom 153 | .find('\n') 154 | .ok_or_else(|| anyhow!("shader marker not valid"))?; 155 | Some(&geom[split_2..]) 156 | } else { 157 | None 158 | }; 159 | 160 | let prg = glr::Program::from_source(gl, vertex, frag, geom)?; 161 | Ok(prg) 162 | } 163 | -------------------------------------------------------------------------------- /src/paper/model/import/waveobj/importer.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use super::super::*; 4 | use super::data; 5 | use cgmath::Zero; 6 | use fxhash::{FxHashMap, FxHashSet}; 7 | use image::DynamicImage; 8 | 9 | pub struct WaveObjImporter { 10 | obj: data::Model, 11 | texture_map: FxHashMap>, 12 | // VertexIndex -> FaceVertex 13 | all_vertices: Vec, 14 | } 15 | 16 | impl WaveObjImporter { 17 | pub fn new(f: R, file_name: &Path) -> Result { 18 | let (matlib, obj) = data::Model::from_reader(f)?; 19 | let matlib = match matlib { 20 | Some(matlib) => Some( 21 | data::solve_find_matlib_file(matlib.as_ref(), file_name) 22 | .ok_or_else(|| anyhow!("{} matlib not found", matlib))?, 23 | ), 24 | None => None, 25 | }; 26 | let mut texture_map = FxHashMap::default(); 27 | 28 | if let Some(matlib) = matlib { 29 | // Textures are read from the .mtl file 30 | let err_mtl = || format!("Error reading matlib file {}", matlib.display()); 31 | let f = std::fs::File::open(&matlib).with_context(err_mtl)?; 32 | let f = std::io::BufReader::new(f); 33 | 34 | for lib in data::Material::from_reader(f).with_context(err_mtl)? { 35 | if let Some(map) = lib.map() { 36 | let err_map = || format!("Error reading texture file {map}"); 37 | if let Some(map) = data::solve_find_matlib_file(map.as_ref(), &matlib) { 38 | let img = image::ImageReader::open(&map) 39 | .with_context(err_map)? 40 | .with_guessed_format() 41 | .with_context(err_map)? 42 | .decode() 43 | .with_context(err_map)?; 44 | let map_name = map 45 | .file_name() 46 | .and_then(|f| f.to_str()) 47 | .ok_or_else(|| anyhow!("Invalid texture name"))?; 48 | texture_map 49 | .insert(lib.name().to_owned(), Cell::new((map_name.to_owned(), img))); 50 | } else { 51 | anyhow::bail!("{} texture from {} matlib not found", map, matlib.display()); 52 | } 53 | } 54 | } 55 | } 56 | 57 | // Remove duplicated vertices by adding them into a set 58 | let all_vertices: FxHashSet = obj 59 | .faces() 60 | .iter() 61 | .flat_map(|f| f.vertices()) 62 | .copied() 63 | .collect(); 64 | //Fix the order into a vector, indexed by VertexIndex 65 | let all_vertices = Vec::from_iter(all_vertices); 66 | 67 | Ok(WaveObjImporter { 68 | obj, 69 | texture_map, 70 | all_vertices, 71 | }) 72 | } 73 | } 74 | 75 | impl Importer for WaveObjImporter { 76 | type VertexId = u32; 77 | 78 | fn vertex_map(&self, i_v: VertexIndex) -> Self::VertexId { 79 | self.all_vertices[usize::from(i_v)].v() 80 | } 81 | fn build_vertices(&self) -> (bool, Vec) { 82 | let mut has_normals = true; 83 | let vs = self 84 | .all_vertices 85 | .iter() 86 | .map(|fv| { 87 | let uv = if let Some(t) = fv.t() { 88 | Vector2::from(*self.obj.texcoord_by_index(t)) 89 | } else { 90 | // If there is no texture coordinates there will be no textures so this value does not matter. 91 | // A zero is easier to work with than an Option. 92 | Vector2::zero() 93 | }; 94 | let normal = if let Some(n) = fv.n() { 95 | Vector3::from(*self.obj.normal_by_index(n)) 96 | } else { 97 | has_normals = false; 98 | Vector3::zero() 99 | }; 100 | Vertex { 101 | pos: Vector3::from(*self.obj.vertex_by_index(fv.v())), 102 | normal, 103 | uv: Vector2::new(uv.x, 1.0 - uv.y), 104 | } 105 | }) 106 | .collect(); 107 | (has_normals, vs) 108 | } 109 | fn face_count(&self) -> usize { 110 | self.obj.faces().len() 111 | } 112 | fn faces(&self) -> impl Iterator, MaterialIndex)> { 113 | self.obj.faces().iter().map(|face| { 114 | let verts: Vec<_> = face 115 | .vertices() 116 | .iter() 117 | .map(|fv| { 118 | VertexIndex::from(self.all_vertices.iter().position(|v| v == fv).unwrap()) 119 | }) 120 | .collect(); 121 | let mat = MaterialIndex::from(face.material()); 122 | (verts, mat) 123 | }) 124 | } 125 | fn build_textures(&self) -> Vec { 126 | let mut textures: Vec<_> = self 127 | .obj 128 | .materials() 129 | .map(|s| { 130 | let tex = &self.texture_map.get(s); 131 | match tex { 132 | Some(tex) => { 133 | let (file_name, pixbuf) = tex.take(); 134 | Texture { 135 | file_name, 136 | pixbuf: Some(pixbuf), 137 | } 138 | } 139 | None => Texture::default(), 140 | } 141 | }) 142 | .collect(); 143 | if textures.is_empty() { 144 | textures.push(Texture::default()); 145 | } 146 | textures 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/paper/model/import/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result, anyhow, bail}; 2 | use std::io::{BufRead, Read}; 3 | use std::panic::catch_unwind; 4 | use std::path::Path; 5 | 6 | use super::{EdgeStatus, Island, MaterialIndex, Model, PaperOptions, Texture, Vertex, VertexIndex}; 7 | use crate::paper::{FlapSide, PageOffset, Papercraft}; 8 | use crate::util_3d::{Vector2, Vector3}; 9 | 10 | pub mod gltf; 11 | pub mod pepakura; 12 | pub mod stl; 13 | pub mod waveobj; 14 | 15 | fn read_u8(rdr: &mut impl Read) -> Result { 16 | let mut x = [0; 1]; 17 | rdr.read_exact(&mut x)?; 18 | Ok(x[0]) 19 | } 20 | fn read_bool(rdr: &mut impl Read) -> Result { 21 | Ok(read_u8(rdr)? != 0) 22 | } 23 | fn read_u16(rdr: &mut impl Read) -> Result { 24 | let mut x = [0; 2]; 25 | rdr.read_exact(&mut x)?; 26 | Ok(u16::from_le_bytes(x)) 27 | } 28 | fn read_u32(rdr: &mut impl Read) -> Result { 29 | let mut x = [0; 4]; 30 | rdr.read_exact(&mut x)?; 31 | Ok(u32::from_le_bytes(x)) 32 | } 33 | fn read_u64(rdr: &mut impl Read) -> Result { 34 | let mut x = [0; 8]; 35 | rdr.read_exact(&mut x)?; 36 | Ok(u64::from_le_bytes(x)) 37 | } 38 | fn read_f32(rdr: &mut impl Read) -> Result { 39 | let mut x = [0; 4]; 40 | rdr.read_exact(&mut x)?; 41 | Ok(f32::from_le_bytes(x)) 42 | } 43 | fn read_f64(rdr: &mut impl Read) -> Result { 44 | let mut x = [0; 8]; 45 | rdr.read_exact(&mut x)?; 46 | Ok(f64::from_le_bytes(x)) 47 | } 48 | fn read_vector2_f64(rdr: &mut impl Read) -> Result { 49 | let x = read_f64(rdr)? as f32; 50 | let y = read_f64(rdr)? as f32; 51 | Ok(Vector2::new(x, y)) 52 | } 53 | fn read_vector3_f64(rdr: &mut impl Read) -> Result { 54 | let x = read_f64(rdr)? as f32; 55 | let y = read_f64(rdr)? as f32; 56 | let z = read_f64(rdr)? as f32; 57 | Ok(Vector3::new(x, y, z)) 58 | } 59 | fn _read_vector2_f32(rdr: &mut impl Read) -> Result { 60 | let x = read_f32(rdr)? as f32; 61 | let y = read_f32(rdr)? as f32; 62 | Ok(Vector2::new(x, y)) 63 | } 64 | fn read_vector3_f32(rdr: &mut impl Read) -> Result { 65 | let x = read_f32(rdr)? as f32; 66 | let y = read_f32(rdr)? as f32; 67 | let z = read_f32(rdr)? as f32; 68 | Ok(Vector3::new(x, y, z)) 69 | } 70 | 71 | pub trait Importer: Sized { 72 | type VertexId: Copy + Eq + std::fmt::Debug; 73 | 74 | fn vertex_map(&self, i_v: VertexIndex) -> Self::VertexId; 75 | // return (has_normals, vertices) 76 | fn build_vertices(&self) -> (bool, Vec); 77 | fn face_count(&self) -> usize; 78 | 79 | fn faces(&self) -> impl Iterator, MaterialIndex)> + '_; 80 | 81 | // Returns at least 1 texture, maybe default. 82 | // As a risky optimization, it can consume the texture data, call only once 83 | fn build_textures(&self) -> Vec; 84 | 85 | // Optional functions 86 | fn compute_edge_status( 87 | &self, 88 | _edge_id: (Self::VertexId, Self::VertexId), 89 | ) -> Option { 90 | None 91 | } 92 | fn relocate_islands<'a>( 93 | &self, 94 | _model: &Model, 95 | _islands: impl Iterator, 96 | ) -> bool { 97 | false 98 | } 99 | fn build_options(&self) -> Option { 100 | None 101 | } 102 | } 103 | 104 | // Returns (model, is_native_format) 105 | pub fn import_model_file(file_name: &Path) -> Result<(Papercraft, bool)> { 106 | // Models have a lot of indices and unwraps, a corrupted file could easily panic 107 | match catch_unwind(|| import_model_file_priv(file_name)) { 108 | Ok(res) => res, 109 | Err(err) => { 110 | if let Some(msg) = err.downcast_ref::<&str>() { 111 | bail!( 112 | "Panic importing the model '{}'!\n{}", 113 | file_name.display(), 114 | msg 115 | ); 116 | } else { 117 | bail!("Panic importing the model '{}'!", file_name.display()); 118 | } 119 | } 120 | } 121 | } 122 | pub fn import_model_file_priv(file_name: &Path) -> Result<(Papercraft, bool)> { 123 | let ext = match file_name.extension() { 124 | None => String::new(), 125 | Some(ext) => { 126 | let mut ext = ext.to_string_lossy().into_owned(); 127 | ext.make_ascii_lowercase(); 128 | ext 129 | } 130 | }; 131 | 132 | let f = std::fs::File::open(file_name) 133 | .with_context(|| format!("Error opening file {}", file_name.display()))?; 134 | let f = std::io::BufReader::new(f); 135 | let mut is_native = false; 136 | 137 | let papercraft = match ext.as_str() { 138 | "craft" => { 139 | is_native = true; 140 | Papercraft::load(f) 141 | .with_context(|| format!("Error reading Papercraft file {}", file_name.display()))? 142 | } 143 | "pdo" => { 144 | let importer = pepakura::PepakuraImporter::new(f) 145 | .with_context(|| format!("Error reading Pepakura file {}", file_name.display()))?; 146 | Papercraft::import(importer) 147 | } 148 | "stl" => { 149 | let importer = stl::StlImporter::new(f) 150 | .with_context(|| format!("Error reading STL file {}", file_name.display()))?; 151 | Papercraft::import(importer) 152 | } 153 | "mtl" => { 154 | anyhow::bail!( 155 | "MTL are material files for OBJ models. Try opening the OBJ file instead." 156 | ); 157 | } 158 | "glb" | "gltf" => { 159 | let importer = gltf::GltfImporter::new(f, file_name) 160 | .with_context(|| format!("Error reading glTF file {}", file_name.display()))?; 161 | Papercraft::import(importer) 162 | } 163 | // "obj" plus unknown extensions are tried as obj, that was the default previously 164 | _ => { 165 | let importer = waveobj::WaveObjImporter::new(f, file_name) 166 | .with_context(|| format!("Error reading Wavefront file {}", file_name.display()))?; 167 | Papercraft::import(importer) 168 | } 169 | }; 170 | Ok((papercraft, is_native)) 171 | } 172 | -------------------------------------------------------------------------------- /thirdparty/afm/names.txt: -------------------------------------------------------------------------------- 1 | .null 0000 2 | CR 000D 3 | space 0020 4 | uni00A0 00A0 5 | A 0041 6 | B 0042 7 | C 0043 8 | D 0044 9 | E 0045 10 | F 0046 11 | G 0047 12 | H 0048 13 | I 0049 14 | J 004A 15 | K 004B 16 | L 004C 17 | M 004D 18 | N 004E 19 | O 004F 20 | P 0050 21 | Q 0051 22 | R 0052 23 | S 0053 24 | T 0054 25 | U 0055 26 | V 0056 27 | W 0057 28 | X 0058 29 | Y 0059 30 | Z 005A 31 | a 0061 32 | b 0062 33 | c 0063 34 | d 0064 35 | e 0065 36 | f 0066 37 | g 0067 38 | h 0068 39 | i 0069 40 | j 006A 41 | k 006B 42 | l 006C 43 | m 006D 44 | n 006E 45 | o 006F 46 | p 0070 47 | q 0071 48 | r 0072 49 | s 0073 50 | t 0074 51 | u 0075 52 | v 0076 53 | w 0077 54 | x 0078 55 | y 0079 56 | z 007A 57 | Agrave 00C0 58 | agrave 00E0 59 | Aacute 00C1 60 | aacute 00E1 61 | Acircumflex 00C2 62 | acircumflex 00E2 63 | Atilde 00C3 64 | atilde 00E3 65 | Adieresis 00C4 66 | adieresis 00E4 67 | Amacron 0100 68 | amacron 0101 69 | Abreve 0102 70 | abreve 0103 71 | Aring 00C5 72 | aring 00E5 73 | Aringacute 01FA 74 | aringacute 01FB 75 | Adotbelow 1EA0 76 | adotbelow 1EA1 77 | Aogonek 0104 78 | aogonek 0105 79 | AE 00C6 80 | ae 00E6 81 | AEacute 01FC 82 | aeacute 01FD 83 | Cacute 0106 84 | cacute 0107 85 | Ccircumflex 0108 86 | ccircumflex 0109 87 | Ccaron 010C 88 | ccaron 010D 89 | Cdotaccent 010A 90 | cdotaccent 010B 91 | Ccedilla 00C7 92 | ccedilla 00E7 93 | Dcaron 010E 94 | dcaron 010F 95 | Dcroat 0110 96 | dcroat 0111 97 | Eth 00D0 98 | eth 00F0 99 | Egrave 00C8 100 | egrave 00E8 101 | Eacute 00C9 102 | eacute 00E9 103 | Ecircumflex 00CA 104 | ecircumflex 00EA 105 | Etilde 1EBC 106 | etilde 1EBD 107 | Ecaron 011A 108 | ecaron 011B 109 | Edieresis 00CB 110 | edieresis 00EB 111 | Emacron 0112 112 | emacron 0113 113 | Ebreve 0114 114 | ebreve 0115 115 | Edotaccent 0116 116 | edotaccent 0117 117 | Edotbelow 1EB8 118 | edotbelow 1EB9 119 | Eogonek 0118 120 | eogonek 0119 121 | Gcircumflex 011C 122 | gcircumflex 011D 123 | Gcaron 01E6 124 | gcaron 01E7 125 | Gbreve 011E 126 | gbreve 011F 127 | Gdotaccent 0120 128 | gdotaccent 0121 129 | Gcommaaccent 0122 130 | gcommaaccent 0123 131 | Hcircumflex 0124 132 | hcircumflex 0125 133 | Hbar 0126 134 | hbar 0127 135 | dotlessi 0131 136 | Igrave 00CC 137 | igrave 00EC 138 | Iacute 00CD 139 | iacute 00ED 140 | Icircumflex 00CE 141 | icircumflex 00EE 142 | Itilde 0128 143 | itilde 0129 144 | Idieresis 00CF 145 | idieresis 00EF 146 | Imacron 012A 147 | imacron 012B 148 | Ibreve 012C 149 | ibreve 012D 150 | Idotaccent 0130 151 | Idotbelow 1ECA 152 | idotbelow 1ECB 153 | Iogonek 012E 154 | iogonek 012F 155 | dotlessj 0237 156 | Jcircumflex 0134 157 | jcircumflex 0135 158 | Kcommaaccent 0136 159 | kcommaaccent 0137 160 | kgreenlandic 0138 161 | Lacute 0139 162 | lacute 013A 163 | Lcaron 013D 164 | lcaron 013E 165 | Lcommaaccent 013B 166 | lcommaaccent 013C 167 | Lslash 0141 168 | lslash 0142 169 | Ldot 013F 170 | ldot 0140 171 | Nacute 0143 172 | nacute 0144 173 | Ntilde 00D1 174 | ntilde 00F1 175 | Ncaron 0147 176 | ncaron 0148 177 | Ncommaaccent 0145 178 | ncommaaccent 0146 179 | Nhookleft 019D 180 | nhookleft 0272 181 | Eng 014A 182 | eng 014B 183 | napostrophe 0149 184 | Ograve 00D2 185 | ograve 00F2 186 | Oacute 00D3 187 | oacute 00F3 188 | Ocircumflex 00D4 189 | ocircumflex 00F4 190 | Otilde 00D5 191 | otilde 00F5 192 | Odieresis 00D6 193 | odieresis 00F6 194 | Omacron 014C 195 | omacron 014D 196 | Obreve 014E 197 | obreve 014F 198 | Ohungarumlaut 0150 199 | ohungarumlaut 0151 200 | Odotbelow 1ECC 201 | odotbelow 1ECD 202 | Oogonek 01EA 203 | oogonek 01EB 204 | Oslash 00D8 205 | oslash 00F8 206 | Oslashacute 01FE 207 | oslashacute 01FF 208 | OE 0152 209 | oe 0153 210 | Racute 0154 211 | racute 0155 212 | Rcaron 0158 213 | rcaron 0159 214 | Rcommaaccent 0156 215 | rcommaaccent 0157 216 | Sacute 015A 217 | sacute 015B 218 | Scircumflex 015C 219 | scircumflex 015D 220 | Scaron 0160 221 | scaron 0161 222 | Scedilla 015E 223 | scedilla 015F 224 | Scommaaccent 0218 225 | scommaaccent 0219 226 | uni1E9E 1E9E 227 | germandbls 00DF 228 | Tcaron 0164 229 | tcaron 0165 230 | uni0162 0162 231 | uni0163 0163 232 | uni021A 021A 233 | uni021B 021B 234 | Tbar 0166 235 | tbar 0167 236 | Thorn 00DE 237 | thorn 00FE 238 | Ugrave 00D9 239 | ugrave 00F9 240 | Uacute 00DA 241 | uacute 00FA 242 | Ucircumflex 00DB 243 | ucircumflex 00FB 244 | Utilde 0168 245 | utilde 0169 246 | Udieresis 00DC 247 | udieresis 00FC 248 | Umacron 016A 249 | umacron 016B 250 | Ubreve 016C 251 | ubreve 016D 252 | Uring 016E 253 | uring 016F 254 | Uhungarumlaut 0170 255 | uhungarumlaut 0171 256 | Udotbelow 1EE4 257 | udotbelow 1EE5 258 | Uogonek 0172 259 | uogonek 0173 260 | Wgrave 1E80 261 | wgrave 1E81 262 | Wacute 1E82 263 | wacute 1E83 264 | Wcircumflex 0174 265 | wcircumflex 0175 266 | Wdieresis 1E84 267 | wdieresis 1E85 268 | Ygrave 1EF2 269 | ygrave 1EF3 270 | Yacute 00DD 271 | yacute 00FD 272 | Ycircumflex 0176 273 | ycircumflex 0177 274 | Ytilde 1EF8 275 | ytilde 1EF9 276 | Ydieresis 0178 277 | ydieresis 00FF 278 | Ymacron 0232 279 | ymacron 0233 280 | Zacute 0179 281 | zacute 017A 282 | Zcaron 017D 283 | zcaron 017E 284 | Zdotaccent 017B 285 | zdotaccent 017C 286 | IJ 0132 287 | ij 0133 288 | IJacute E133 289 | ijacute E132 290 | Schwa 018F 291 | schwa 0259 292 | f_f FB00 293 | fi FB01 294 | fl FB02 295 | f_ij EB01 296 | f_f_i FB03 297 | f_f_l FB04 298 | f_f_ij EB03 299 | ampersand 0026 300 | at 0040 301 | asterisk 002A 302 | copyright 00A9 303 | registered 00AE 304 | trademark 2122 305 | asciicircum 005E 306 | asciitilde 007E 307 | grave 0060 308 | acute 00B4 309 | circumflex 02C6 310 | caron 02C7 311 | tilde 02DC 312 | dieresis 00A8 313 | macron 00AF 314 | uni02C9 02C9 315 | breve 02D8 316 | ring 02DA 317 | hungarumlaut 02DD 318 | dotaccent 02D9 319 | cedilla 00B8 320 | dotbelowcomb 0323 321 | ogonek 02DB 322 | commaaccent 0326 323 | uni00AD 00AD 324 | hyphen 002D 325 | endash 2013 326 | emdash 2014 327 | underscore 005F 328 | period 002E 329 | comma 002C 330 | colon 003A 331 | semicolon 003B 332 | exclam 0021 333 | exclamdown 00A1 334 | question 003F 335 | questiondown 00BF 336 | ellipsis 2026 337 | periodcentered 00B7 338 | uni2219 2219 339 | bullet 2022 340 | slash 002F 341 | backslash 005C 342 | bar 007C 343 | brokenbar 00A6 344 | parenleft 0028 345 | parenright 0029 346 | bracketleft 005B 347 | bracketright 005D 348 | braceleft 007B 349 | braceright 007D 350 | quotesingle 0027 351 | quotedbl 0022 352 | quoteleft 2018 353 | quoteright 2019 354 | quotedblleft 201C 355 | quotedblright 201D 356 | quotesinglbase 201A 357 | quotedblbase 201E 358 | guilsinglleft 2039 359 | guilsinglright 203A 360 | guillemotleft 00AB 361 | guillemotright 00BB 362 | paragraph 00B6 363 | uniF8FF F8FF 364 | numbersign 0023 365 | zero 0030 366 | one 0031 367 | two 0032 368 | three 0033 369 | four 0034 370 | five 0035 371 | six 0036 372 | seven 0037 373 | eight 0038 374 | nine 0039 375 | fraction 2044 376 | uni2215 2215 377 | onequarter 00BC 378 | onehalf 00BD 379 | threequarters 00BE 380 | degree 00B0 381 | percent 0025 382 | perthousand 2030 383 | plus 002B 384 | minus 2212 385 | plusminus 00B1 386 | equal 003D 387 | notequal 2260 388 | approxequal 2248 389 | multiply 00D7 390 | less 003C 391 | greater 003E 392 | lessequal 2264 393 | greaterequal 2265 394 | divide 00F7 395 | logicalnot 00AC 396 | dagger 2020 397 | daggerdbl 2021 398 | section 00A7 399 | Euro 20AC 400 | currency 00A4 401 | dollar 0024 402 | cent 00A2 403 | florin 0192 404 | sterling 00A3 405 | yen 00A5 406 | onesuperior 00B9 407 | twosuperior 00B2 408 | threesuperior 00B3 409 | ordfeminine 00AA 410 | ordmasculine 00BA 411 | uni2116 2116 412 | Delta 2206 413 | Deltagreek 0394 414 | Omega 2126 415 | Omegagreek 03A9 416 | mu 00B5 417 | mugreek 03BC 418 | pi 03C0 419 | uni2113 2113 420 | estimated 212E 421 | infinity 221E 422 | partialdiff 2202 423 | integral 222B 424 | radical 221A 425 | summation 2211 426 | product 220F 427 | lozenge 25CA 428 | -------------------------------------------------------------------------------- /papercraft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 48 | 51 | 57 | 65 | 71 | 73 | 79 | 85 | 91 | 97 | 103 | 109 | 115 | 121 | 127 | 133 | 139 | 145 | 151 | 157 | 163 | 169 | 175 | 181 | 187 | 193 | 199 | 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /src/paper/model/import/pepakura/importer.rs: -------------------------------------------------------------------------------- 1 | use super::super::*; 2 | use super::data; 3 | use cgmath::{Deg, InnerSpace, Rad}; 4 | use image::{DynamicImage, ImageBuffer}; 5 | use std::cell::Cell; 6 | 7 | pub struct PepakuraImporter { 8 | pdo: data::Pdo, 9 | //VertexIndex -> (obj_id, face_id, vert_in_face) 10 | vertex_map: Vec<(u32, u32, u32)>, 11 | options: PaperOptions, 12 | 13 | // We won't know the page layout until after computing the islands 14 | pages: Cell<(u32, u32)>, 15 | } 16 | 17 | impl PepakuraImporter { 18 | pub fn new(f: R) -> Result { 19 | let pdo = data::Pdo::from_reader(f)?; 20 | 21 | let vertex_map: Vec<(u32, u32, u32)> = pdo 22 | .objects() 23 | .iter() 24 | .enumerate() 25 | .flat_map(|(i_o, obj)| { 26 | obj.faces.iter().enumerate().flat_map(move |(i_f, f)| { 27 | (0..f.verts.len()).map(move |i_vf| (i_o as u32, i_f as u32, i_vf as u32)) 28 | }) 29 | }) 30 | .collect(); 31 | 32 | let settings = pdo.settings(); 33 | let margin = Vector2::new(settings.margin_side as f32, settings.margin_top as f32); 34 | let page_size = settings.page_size; 35 | 36 | let mut options = PaperOptions { 37 | page_size: (page_size.x, page_size.y), 38 | margin: (margin.y, margin.x, margin.x, margin.y), 39 | ..Default::default() 40 | }; 41 | if let Some(a) = settings.fold_line_hide_angle { 42 | options.hidden_line_angle = (180 - a) as f32; 43 | } 44 | if let Some(unfold) = pdo.unfold() { 45 | options.scale = unfold.scale; 46 | } 47 | 48 | Ok(PepakuraImporter { 49 | pdo, 50 | vertex_map, 51 | options, 52 | pages: Cell::new((1, 1)), 53 | }) 54 | } 55 | } 56 | impl Importer for PepakuraImporter { 57 | // (obj_id, vertex_id) 58 | type VertexId = (u32, u32); 59 | 60 | fn build_vertices(&self) -> (bool, Vec) { 61 | let vs = self 62 | .vertex_map 63 | .iter() 64 | .map(|&(i_o, i_f, i_vf)| { 65 | let obj = &self.pdo.objects()[i_o as usize]; 66 | let f = &obj.faces[i_f as usize]; 67 | let v_f = &f.verts[i_vf as usize]; 68 | let v = &obj.vertices[v_f.i_v as usize]; 69 | 70 | Vertex { 71 | pos: v.v, 72 | normal: f.normal, 73 | uv: v_f.uv, 74 | } 75 | }) 76 | .collect(); 77 | (true, vs) 78 | } 79 | fn vertex_map(&self, i_v: VertexIndex) -> Self::VertexId { 80 | let (i_o, i_f, i_vf) = self.vertex_map[usize::from(i_v)]; 81 | let i_v = self.pdo.objects()[i_o as usize].faces[i_f as usize].verts[i_vf as usize].i_v; 82 | (i_o, i_v) 83 | } 84 | fn face_count(&self) -> usize { 85 | self.pdo.objects().iter().map(|o| o.faces.len()).sum() 86 | } 87 | fn faces(&self) -> impl Iterator, MaterialIndex)> { 88 | self.pdo 89 | .objects() 90 | .iter() 91 | .enumerate() 92 | .flat_map(move |(obj_id, obj)| { 93 | let obj_id = obj_id as u32; 94 | obj.faces.iter().enumerate().map(move |(face_id, face)| { 95 | let face_id = face_id as u32; 96 | let verts: Vec = (0..face.verts.len()) 97 | .map(|v_f| { 98 | let id = (obj_id, face_id, v_f as u32); 99 | let i = self.vertex_map.iter().position(|x| x == &id).unwrap(); 100 | VertexIndex::from(i) 101 | }) 102 | .collect(); 103 | // We will add a default material at the end of the textures, so map any out-of bounds to that 104 | let mat_index = face.mat_index.min(self.pdo.materials().len() as u32); 105 | let mat = MaterialIndex::from(mat_index as usize); 106 | (verts, mat) 107 | }) 108 | }) 109 | } 110 | fn build_textures(&self) -> Vec { 111 | let mut textures: Vec<_> = self 112 | .pdo 113 | .materials() 114 | .iter() 115 | .map(|mat| { 116 | let pixbuf = mat.texture.as_ref().and_then(|t| { 117 | let img = ImageBuffer::from_raw(t.width, t.height, t.data.take()); 118 | img.map(DynamicImage::ImageRgb8) 119 | }); 120 | Texture { 121 | file_name: mat.name.clone() + ".png", 122 | pixbuf, 123 | } 124 | }) 125 | .collect(); 126 | textures.push(Texture::default()); 127 | textures 128 | } 129 | fn compute_edge_status(&self, edge_id: (Self::VertexId, Self::VertexId)) -> Option { 130 | let ((obj_id, v0_id), (_, v1_id)) = edge_id; 131 | let vv = (v0_id, v1_id); 132 | let obj = &self.pdo.objects()[obj_id as usize]; 133 | let edge = obj 134 | .edges 135 | .iter() 136 | .find(|&e| vv == (e.i_v1, e.i_v2) || vv == (e.i_v2, e.i_v1))?; 137 | if edge.connected { 138 | Some(EdgeStatus::Joined) 139 | } else { 140 | let v_f = obj.faces[edge.i_f1 as usize] 141 | .verts 142 | .iter() 143 | .find(|v_f| v_f.i_v == edge.i_v1) 144 | .unwrap(); 145 | if v_f.flap.is_some() { 146 | Some(EdgeStatus::Cut(FlapSide::True)) 147 | } else { 148 | None 149 | } 150 | } 151 | } 152 | fn relocate_islands<'a>( 153 | &self, 154 | model: &Model, 155 | islands: impl Iterator, 156 | ) -> bool { 157 | let Some(unfold) = self.pdo.unfold() else { 158 | return false; 159 | }; 160 | 161 | let margin = Vector2::new(self.options.margin.1, self.options.margin.0); 162 | let area_size = Vector2::from(self.options.page_size) - 2.0 * margin; 163 | 164 | let mut n_cols = 0; 165 | let mut max_page = (0, 0); 166 | for island in islands { 167 | let face = &model[island.root_face()]; 168 | let [i_v0, i_v1, _] = face.index_vertices(); 169 | let (ip_obj, ip_face, ip_v0) = self.vertex_map[usize::from(i_v0)]; 170 | let (_, _, ip_v1) = self.vertex_map[usize::from(i_v1)]; 171 | let p_face = &self.pdo.objects()[ip_obj as usize].faces[ip_face as usize]; 172 | let vf0 = p_face.verts[ip_v0 as usize].pos2d; 173 | let vf1 = p_face.verts[ip_v1 as usize].pos2d; 174 | let i_part = p_face.part_index; 175 | 176 | let normal = model.face_plane(face); 177 | let pv0 = normal.project(&model[i_v0].pos(), self.options.scale); 178 | let pv1 = normal.project(&model[i_v1].pos(), self.options.scale); 179 | 180 | let part = &unfold.parts[i_part as usize]; 181 | 182 | let rot = (pv1 - pv0).angle(vf1 - vf0); 183 | let loc = vf0 - pv0 + part.bb.v0; 184 | 185 | let mut col = loc.x.div_euclid(area_size.x) as i32; 186 | let mut row = loc.y.div_euclid(area_size.y) as i32; 187 | let loc = Vector2::new(loc.x.rem_euclid(area_size.x), loc.y.rem_euclid(area_size.y)); 188 | let loc = loc + margin; 189 | 190 | // Some models use negative pages to hide pieces 191 | if col < 0 || row < 0 { 192 | col = -1; 193 | row = 0; 194 | } else { 195 | let row = row as u32; 196 | let col = col as u32; 197 | n_cols = n_cols.max(col); 198 | if row > max_page.0 || (row == max_page.0 && col > max_page.1) { 199 | max_page = (row, col); 200 | } 201 | } 202 | 203 | let loc = self.options.page_to_global(PageOffset { 204 | row, 205 | col, 206 | offset: loc, 207 | }); 208 | island.reset_transformation(island.root_face(), rot, loc); 209 | } 210 | // 0-based 211 | let page_cols = n_cols + 1; 212 | let pages = max_page.0 * page_cols + max_page.1 + 1; 213 | 214 | self.pages.set((page_cols, pages)); 215 | true 216 | } 217 | fn build_options(&self) -> Option { 218 | let mut options = self.options.clone(); 219 | let (page_cols, pages) = self.pages.get(); 220 | options.page_cols = page_cols; 221 | options.pages = pages; 222 | 223 | // We don't have options per flap, yet. Do an average instead. 224 | let mut flap_count = 0; 225 | let mut flap_width = 0.0; 226 | let mut flap_angle = 0.0; 227 | for obj in self.pdo.objects() { 228 | for face in &obj.faces { 229 | for vert in &face.verts { 230 | if let Some(flap) = &vert.flap { 231 | flap_width += flap.width; 232 | flap_count += 1; 233 | flap_angle += flap.angle1.0 + flap.angle2.0; 234 | } 235 | } 236 | } 237 | } 238 | if flap_count > 0 { 239 | let flap_width = flap_width / flap_count as f32; 240 | let flap_angle = Deg::from(Rad(flap_angle / 2.0 / flap_count as f32)).0; 241 | options.flap_width = flap_width.round(); 242 | options.flap_angle = flap_angle.round(); 243 | } 244 | 245 | Some(options) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/paper/model/import/waveobj/data.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use std::{ 3 | io::BufRead, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] 8 | pub struct FaceVertex { 9 | v: u32, 10 | t: Option, 11 | n: Option, 12 | } 13 | 14 | #[derive(Clone, Debug)] 15 | pub struct Face { 16 | material: usize, 17 | verts: Vec, 18 | } 19 | 20 | #[derive(Clone, Debug)] 21 | pub struct Model { 22 | materials: Vec, 23 | vs: Vec<[f32; 3]>, 24 | ns: Vec<[f32; 3]>, 25 | ts: Vec<[f32; 2]>, 26 | faces: Vec, 27 | } 28 | 29 | impl Model { 30 | //Returns (matlib, model) 31 | pub fn from_reader(r: R) -> Result<(Option, Model)> { 32 | let syn_error = || anyhow!("invalid obj syntax"); 33 | 34 | let mut material_lib = None; 35 | let mut current_material: usize = 0; 36 | let mut data = Model { 37 | materials: Vec::new(), 38 | vs: Vec::new(), 39 | ns: Vec::new(), 40 | ts: Vec::new(), 41 | faces: Vec::new(), 42 | }; 43 | 44 | for line in r.lines() { 45 | let line = line?; 46 | let line = line.trim(); 47 | //skip empty and comments 48 | if line.is_empty() || line.starts_with('#') { 49 | continue; 50 | } 51 | let mut words = line.split_whitespace(); 52 | let first = words.next().ok_or_else(syn_error)?; 53 | match first { 54 | "o" => { 55 | // We combine all the objects into one. 56 | // Fortunately the numbering of vertices and faces is global to the file not to the object, so nothing to do here. 57 | } 58 | "v" => { 59 | let x: f32 = words.next().ok_or_else(syn_error)?.parse()?; 60 | let y: f32 = words.next().ok_or_else(syn_error)?.parse()?; 61 | let z: f32 = words.next().ok_or_else(syn_error)?.parse()?; 62 | data.vs.push([x, y, z]); 63 | } 64 | "vt" => { 65 | let u: f32 = words.next().ok_or_else(syn_error)?.parse()?; 66 | let v: f32 = words.next().ok_or_else(syn_error)?.parse()?; 67 | data.ts.push([u, v]); 68 | } 69 | "vn" => { 70 | let x: f32 = words.next().ok_or_else(syn_error)?.parse()?; 71 | let y: f32 = words.next().ok_or_else(syn_error)?.parse()?; 72 | let z: f32 = words.next().ok_or_else(syn_error)?.parse()?; 73 | data.ns.push([x, y, z]); 74 | } 75 | "f" => { 76 | let mut verts = Vec::new(); 77 | for fv in words { 78 | let mut vals = fv.split('/'); 79 | let v = vals.next().ok_or_else(syn_error)?.parse::()? - 1; 80 | let t = vals 81 | .next() 82 | .and_then(|x| x.parse::().ok()) 83 | .map(|x| x - 1); 84 | let n = vals 85 | .next() 86 | .and_then(|x| x.parse::().ok()) 87 | .map(|x| x - 1); 88 | if v >= data.vs.len() { 89 | anyhow::bail!("vertex index out of range"); 90 | } 91 | if matches!(t, Some(t) if t >= data.ts.len()) { 92 | anyhow::bail!("texture index out of range"); 93 | } 94 | if matches!(n, Some(n) if n >= data.ns.len()) { 95 | anyhow::bail!("normal index out of range"); 96 | } 97 | let v = FaceVertex { 98 | v: v as u32, 99 | t: t.map(|t| t as u32), 100 | n: n.map(|n| n as u32), 101 | }; 102 | verts.push(v); 103 | } 104 | data.faces.push(Face { 105 | material: current_material, 106 | verts, 107 | }) 108 | } 109 | "mtllib" => { 110 | // keep spaces in the file name 111 | let lib = line 112 | .find(char::is_whitespace) 113 | .map(|idx| &line[idx + 1..]) 114 | .ok_or_else(syn_error)?; 115 | material_lib = Some(lib.to_owned()); 116 | } 117 | "usemtl" => { 118 | let mtl = words.next().ok_or_else(syn_error)?; 119 | if let Some(p) = data.materials.iter().position(|m| m == mtl) { 120 | current_material = p; 121 | } else { 122 | current_material = data.materials.len(); 123 | data.materials.push(String::from(mtl)); 124 | } 125 | } 126 | "s" => { /* smoothing is ignored */ } 127 | _p => { 128 | // Unknown attribute 129 | //println!("{_p}??"); 130 | } 131 | } 132 | } 133 | Ok((material_lib, data)) 134 | } 135 | pub fn materials(&self) -> impl Iterator + '_ { 136 | self.materials.iter().map(|s| &s[..]) 137 | } 138 | pub fn faces(&self) -> &[Face] { 139 | &self.faces 140 | } 141 | pub fn vertex_by_index(&self, idx: u32) -> &[f32; 3] { 142 | &self.vs[idx as usize] 143 | } 144 | pub fn normal_by_index(&self, idx: u32) -> &[f32; 3] { 145 | &self.ns[idx as usize] 146 | } 147 | pub fn texcoord_by_index(&self, idx: u32) -> &[f32; 2] { 148 | &self.ts[idx as usize] 149 | } 150 | } 151 | 152 | impl Face { 153 | pub fn material(&self) -> usize { 154 | self.material 155 | } 156 | pub fn vertices(&self) -> &[FaceVertex] { 157 | &self.verts 158 | } 159 | } 160 | 161 | impl FaceVertex { 162 | pub fn v(&self) -> u32 { 163 | self.v 164 | } 165 | pub fn n(&self) -> Option { 166 | self.n 167 | } 168 | pub fn t(&self) -> Option { 169 | self.t 170 | } 171 | } 172 | 173 | pub fn solve_find_matlib_file(mtl: &Path, obj: &Path) -> Option { 174 | let obj_dir = match obj.parent() { 175 | None => ".".into(), 176 | Some(d) => d.to_owned(), 177 | }; 178 | if mtl.is_relative() { 179 | // First find the mtl in the same directory as the obj, using the local path 180 | let mut dir = obj_dir.clone(); 181 | dir.push(mtl); 182 | if dir.exists() { 183 | return Some(dir); 184 | } 185 | // Then without the mtl path 186 | dir = obj_dir; 187 | dir.push(mtl.file_name()?); 188 | if dir.exists() { 189 | return Some(dir); 190 | } 191 | } else { 192 | // If mtl is absolute, first try the real file 193 | if mtl.exists() { 194 | return Some(mtl.to_owned()); 195 | } 196 | // Then try the same name in a local path 197 | let mut dir = obj_dir; 198 | dir.push(mtl.file_name()?); 199 | if dir.exists() { 200 | return Some(dir); 201 | } 202 | } 203 | None 204 | } 205 | 206 | #[derive(Clone, Debug)] 207 | pub struct Material { 208 | name: String, 209 | map: Option, 210 | } 211 | 212 | impl Material { 213 | pub fn from_reader(r: R) -> Result> { 214 | let syn_error = || anyhow!("invalid mtl syntax"); 215 | 216 | let mut mats = Vec::new(); 217 | 218 | #[derive(Default)] 219 | struct MaterialData { 220 | name: Option, 221 | map: Option, 222 | } 223 | 224 | impl MaterialData { 225 | fn build(&mut self) -> Option { 226 | if self.name.is_none() { 227 | *self = MaterialData::default(); 228 | return None; 229 | } 230 | 231 | let m = Material { 232 | name: self.name.take().unwrap(), 233 | map: self.map.take(), 234 | }; 235 | 236 | Some(m) 237 | } 238 | } 239 | let mut data = MaterialData::default(); 240 | 241 | for line in r.lines() { 242 | let line = line?; 243 | let line = line.trim(); 244 | //skip empty and comments 245 | if line.is_empty() || line.starts_with('#') { 246 | continue; 247 | } 248 | let mut words = line.split_whitespace(); 249 | let first = words.next().ok_or_else(syn_error)?; 250 | match first { 251 | "newmtl" => { 252 | mats.extend(data.build()); 253 | 254 | let name = words.next().ok_or_else(syn_error)?; 255 | data.name = Some(String::from(name)); 256 | } 257 | "map_Kd" => { 258 | // keep spaces in the file name 259 | let map = line 260 | .find(char::is_whitespace) 261 | .map(|idx| &line[idx + 1..]) 262 | .ok_or_else(syn_error)?; 263 | data.map = Some(String::from(map)); 264 | } 265 | _p => { 266 | // Unknown attribute 267 | //println!("{_p}??"); 268 | } 269 | } 270 | } 271 | mats.extend(data.build()); 272 | Ok(mats) 273 | } 274 | pub fn map(&self) -> Option<&str> { 275 | self.map.as_deref() 276 | } 277 | pub fn name(&self) -> &str { 278 | &self.name 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 39 | 44 | 45 | 47 | 52 | 55 | 58 | 65 | 66 | 67 | 70 | 73 | 80 | 81 | 82 | 85 | 88 | 95 | 96 | 97 | 100 | 103 | 110 | 111 | 112 | 115 | 117 | 124 | 125 | 126 | 129 | 131 | 138 | 139 | 140 | 141 | 145 | 150 | 153 | 158 | 163 | 167 | 171 | 174 | 179 | 184 | 189 | 190 | 191 | 195 | 200 | 205 | 210 | 212 | 217 | 222 | 227 | 228 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /src/util_3d.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{InnerSpace, MetricSpace, Rad, Zero}; 2 | use std::f32::consts::PI; 3 | 4 | pub type Vector2 = cgmath::Vector2; 5 | pub type Vector3 = cgmath::Vector3; 6 | pub type Point2 = cgmath::Point2; 7 | pub type Point3 = cgmath::Point3; 8 | pub type Quaternion = cgmath::Quaternion; 9 | pub type Matrix2 = cgmath::Matrix2; 10 | pub type Matrix3 = cgmath::Matrix3; 11 | pub type Matrix4 = cgmath::Matrix4; 12 | 13 | #[derive(Debug)] 14 | pub struct Plane { 15 | origin: Vector3, 16 | base_x: Vector3, 17 | base_y: Vector3, 18 | } 19 | 20 | impl Default for Plane { 21 | fn default() -> Plane { 22 | Plane { 23 | origin: Vector3::zero(), 24 | base_x: Vector3::new(1.0, 0.0, 0.0), 25 | base_y: Vector3::new(0.0, 1.0, 0.0), 26 | } 27 | } 28 | } 29 | 30 | impl Plane { 31 | pub fn project(&self, p: &Vector3, scale: f32) -> Vector2 { 32 | let p = p - self.origin; 33 | let x = p.dot(self.base_x); 34 | let y = p.dot(self.base_y); 35 | scale * Vector2::new(x, y) 36 | } 37 | pub fn from_tri(tri: [Vector3; 3]) -> Plane { 38 | let v0 = tri[1] - tri[0]; 39 | let v1 = tri[2] - tri[0]; 40 | let normal = v0.cross(v1); 41 | Plane { 42 | origin: tri[0], 43 | base_x: v0.normalize(), 44 | base_y: v0.cross(normal).normalize(), 45 | } 46 | } 47 | pub fn normal(&self) -> Vector3 { 48 | self.base_x.cross(self.base_y) 49 | } 50 | } 51 | 52 | // Each returned tuple is a triangle of indices into the original vector 53 | pub fn tessellate(ps: &[Vector3]) -> (Vec<[usize; 3]>, Plane) { 54 | if ps.len() < 3 { 55 | return (Vec::new(), Plane::default()); 56 | } 57 | 58 | // Compute the face plane 59 | let mut normal = Vector3::zero(); 60 | for i in 0..ps.len() { 61 | let a = ps[i]; 62 | let b = ps[(i + 1) % ps.len()]; 63 | normal += a.cross(b); 64 | } 65 | 66 | let normal = normal.normalize(); 67 | let plane_x = (ps[1] - ps[0]).normalize(); 68 | let plane_y = plane_x.cross(normal); 69 | let plane_o = ps[0]; 70 | 71 | let plane = Plane { 72 | origin: plane_o, 73 | base_x: plane_x, 74 | base_y: plane_y, 75 | }; 76 | 77 | if ps.len() == 3 { 78 | return (vec![[0, 1, 2]], plane); 79 | } 80 | 81 | let mut res = Vec::with_capacity(ps.len() - 2); 82 | 83 | // Project every vertex into this plane 84 | let mut ps = ps 85 | .iter() 86 | .enumerate() 87 | .map(|(idx, p)| { 88 | let p2 = plane.project(p, 1.0); 89 | (idx, p2) 90 | }) 91 | .collect::>(); 92 | 93 | // Tessellate the 2D polygon using the "ear" method 94 | while ps.len() >= 3 { 95 | let mut min_angle = None; 96 | 97 | for i in 0..ps.len() { 98 | let (_, a) = ps[i]; 99 | let (_, b) = ps[(i + 1) % ps.len()]; 100 | let (_, c) = ps[(i + 2) % ps.len()]; 101 | let angle = (c - b).angle(b - a); 102 | 103 | // Find the vertex with the minimum inner angle 104 | let inner_angle = Rad(PI) - angle; 105 | 106 | if min_angle.map(|(_, a)| inner_angle < a).unwrap_or(true) { 107 | // If this point is not an ear, discard it 108 | if !ps.iter().enumerate().any(|(i_other, (_, p_other))| { 109 | i_other != i 110 | && i_other != (i + 1) % ps.len() 111 | && i_other != (i + 2) % ps.len() 112 | && point_in_triangle(*p_other, [a, b, c]) 113 | }) { 114 | min_angle = Some((i, inner_angle)); 115 | } 116 | } 117 | } 118 | // min_angle should never be None, but just in case 119 | let i = min_angle.map(|(i, _)| i).unwrap_or(0); 120 | 121 | let tri = (i, (i + 1) % ps.len(), (i + 2) % ps.len()); 122 | res.push([ps[tri.0].0, ps[tri.1].0, ps[tri.2].0]); 123 | ps.remove(tri.1); 124 | } 125 | 126 | (res, plane) 127 | } 128 | 129 | pub fn point_in_triangle(p: Vector2, tri: [Vector2; 3]) -> bool { 130 | let [p0, p1, p2] = tri; 131 | let s = (p0.x - p2.x) * (p.y - p2.y) - (p0.y - p2.y) * (p.x - p2.x); 132 | let t = (p1.x - p0.x) * (p.y - p0.y) - (p1.y - p0.y) * (p.x - p0.x); 133 | 134 | if (s < 0.0) != (t < 0.0) && s != 0.0 && t != 0.0 { 135 | false 136 | } else { 137 | let d = (p2.x - p1.x) * (p.y - p1.y) - (p2.y - p1.y) * (p.x - p1.x); 138 | d == 0.0 || (d < 0.0) == (s + t <= 0.0) 139 | } 140 | } 141 | 142 | pub fn bounding_box_3d(vs: impl IntoIterator) -> (Vector3, Vector3) { 143 | let mut vs = vs.into_iter(); 144 | let (mut a, mut b) = match vs.next() { 145 | Some(v) => (v, v), 146 | None => return (Vector3::zero(), Vector3::zero()), 147 | }; 148 | for v in vs { 149 | a.x = a.x.min(v.x); 150 | a.y = a.y.min(v.y); 151 | a.z = a.z.min(v.z); 152 | b.x = b.x.max(v.x); 153 | b.y = b.y.max(v.y); 154 | b.z = b.z.max(v.z); 155 | } 156 | (a, b) 157 | } 158 | 159 | pub fn bounding_box_2d(vs: impl IntoIterator) -> (Vector2, Vector2) { 160 | let mut vs = vs.into_iter(); 161 | let (mut a, mut b) = match vs.next() { 162 | Some(v) => (v, v), 163 | None => return (Vector2::zero(), Vector2::zero()), 164 | }; 165 | for v in vs { 166 | a.x = a.x.min(v.x); 167 | a.y = a.y.min(v.y); 168 | b.x = b.x.max(v.x); 169 | b.y = b.y.max(v.y); 170 | } 171 | (a, b) 172 | } 173 | 174 | pub fn ray_crosses_face(ray: (Vector3, Vector3), vs: &[Vector3; 3]) -> Option { 175 | // Möller-Trumbore algorithm 176 | 177 | let v0v1 = vs[1] - vs[0]; 178 | let v0v2 = vs[2] - vs[0]; 179 | let dir = ray.1 - ray.0; 180 | let pvec = dir.cross(v0v2); 181 | let det = v0v1.dot(pvec); 182 | 183 | //backface culling? 184 | /*if (det < 0.0001) { 185 | return None; 186 | }*/ 187 | 188 | // ray and triangle are parallel if det is close to 0 189 | if det.abs() < f32::EPSILON { 190 | return None; 191 | } 192 | 193 | let inv_det = 1.0 / det; 194 | 195 | let tvec = ray.0 - vs[0]; 196 | let u = tvec.dot(pvec) * inv_det; 197 | if !(0.0..=1.0).contains(&u) { 198 | return None; 199 | } 200 | 201 | let qvec = tvec.cross(v0v1); 202 | let v = dir.dot(qvec) * inv_det; 203 | if v < 0.0 || u + v > 1.0 { 204 | return None; 205 | } 206 | 207 | let t = v0v2.dot(qvec) * inv_det; 208 | 209 | Some(t) 210 | } 211 | 212 | // Returns (offset0, offset1, distance2) 213 | pub fn line_line_distance(line0: (Vector3, Vector3), line1: (Vector3, Vector3)) -> (f32, f32, f32) { 214 | let diff = line0.0 - line1.0; 215 | let line0d = line0.1 - line0.0; 216 | let line1d = line1.1 - line1.0; 217 | let len0 = line0d.magnitude(); 218 | let len1 = line1d.magnitude(); 219 | let line0d = line0d / len0; 220 | let line1d = line1d / len1; 221 | let a01 = -line0d.dot(line1d); 222 | let b0 = line0d.dot(diff); 223 | let c = diff.magnitude2(); 224 | let det = 1.0 - a01 * a01; 225 | let l0_closest; 226 | let l1_closest; 227 | let distance2; 228 | if det.abs() > f32::EPSILON { 229 | //not parallel 230 | let b1 = -line1d.dot(diff); 231 | let inv_det = 1.0 / det; 232 | l0_closest = (a01 * b1 - b0) * inv_det; 233 | l1_closest = (a01 * b0 - b1) * inv_det; 234 | distance2 = l0_closest * (l0_closest + a01 * l1_closest + 2.0 * b0) 235 | + l1_closest * (a01 * l0_closest + l1_closest + 2.0 * b1) 236 | + c; 237 | } else { 238 | //almost parallel 239 | l0_closest = -b0; 240 | l1_closest = 0.0; 241 | distance2 = b0 * l0_closest + c; 242 | } 243 | (l0_closest / len0, l1_closest / len1, distance2.abs()) 244 | } 245 | 246 | pub fn line_segment_distance( 247 | line0: (Vector3, Vector3), 248 | line1: (Vector3, Vector3), 249 | ) -> (f32, f32, f32) { 250 | let (l0_closest, mut l1_closest, mut distance2) = line_line_distance(line0, line1); 251 | if l1_closest < 0.0 { 252 | l1_closest = 0.0; 253 | let p = line0.0 + (line0.1 - line0.0) * l0_closest; 254 | distance2 = p.distance2(line1.0); 255 | } else if l1_closest > 1.0 { 256 | l1_closest = 1.0; 257 | let p = line0.0 + (line0.1 - line0.0) * l0_closest; 258 | distance2 = p.distance2(line1.1); 259 | } 260 | (l0_closest, l1_closest, distance2) 261 | } 262 | 263 | // Returns (offset, distance) 264 | pub fn point_line_distance(p: Vector2, line: (Vector2, Vector2)) -> (f32, f32) { 265 | let vline = line.1 - line.0; 266 | let line_len = vline.magnitude(); 267 | let vline = vline / line_len; 268 | let vline_perp = Vector2::new(vline.y, -vline.x); 269 | let p = p - line.0; 270 | let o = p.dot(vline) / line_len; 271 | let d = p.dot(vline_perp).abs(); 272 | (o, d) 273 | } 274 | 275 | pub fn point_line_side(p: Vector2, line: (Vector2, Vector2)) -> bool { 276 | let v1 = line.1 - line.0; 277 | let v2 = p - line.0; 278 | (v1.x * v2.y - v1.y * v2.x) >= 0.0 279 | } 280 | 281 | // (offset, distance) 282 | pub fn point_segment_distance(p: Vector2, line: (Vector2, Vector2)) -> (f32, f32) { 283 | let (o, d) = point_line_distance(p, line); 284 | if o < 0.0 { 285 | (0.0, p.distance(line.0)) 286 | } else if o > 1.0 { 287 | (1.0, p.distance(line.1)) 288 | } else { 289 | (o, d) 290 | } 291 | } 292 | 293 | // (intersection, offset_1, offset_2) 294 | pub fn line_line_intersection( 295 | line_1: (Vector2, Vector2), 296 | line_2: (Vector2, Vector2), 297 | ) -> (Vector2, f32, f32) { 298 | let s1 = line_1.1 - line_1.0; 299 | let s2 = line_2.1 - line_2.0; 300 | let d = line_1.0 - line_2.0; 301 | 302 | let dd = s1.x * s2.y - s2.x * s1.y; 303 | if dd.abs() < 0.000001 { 304 | return (line_1.0, f32::MAX, f32::MAX); 305 | } 306 | 307 | let s = (s2.x * d.y - s2.y * d.x) / dd; 308 | let t = (s1.x * d.y - s1.y * d.x) / dd; 309 | 310 | ((line_1.0 + s * s1), s, t) 311 | } 312 | 313 | pub fn ortho2d(width: f32, height: f32) -> Matrix3 { 314 | let right = width / 2.0; 315 | let left = -right; 316 | let top = -height / 2.0; 317 | let bottom = -top; 318 | Matrix3::new( 319 | 2.0 / (right - left), 320 | 0.0, 321 | 0.0, 322 | 0.0, 323 | 2.0 / (top - bottom), 324 | 0.0, 325 | 0.0, 326 | 0.0, 327 | 1.0, 328 | ) 329 | } 330 | 331 | // Like ortho2d but aligned to the (0,0) 332 | pub fn ortho2d_zero(width: f32, height: f32) -> Matrix3 { 333 | Matrix3::new( 334 | 2.0 / width, 335 | 0.0, 336 | 0.0, 337 | 0.0, 338 | -2.0 / height, 339 | 0.0, 340 | -1.0, 341 | -1.0, 342 | 1.0, 343 | ) 344 | } 345 | -------------------------------------------------------------------------------- /locales/messages.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2025-12-01 20:12+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/printable.rs:42 21 | msgid "Don't know how to write the format of {}" 22 | msgstr "" 23 | 24 | #: src/printable.rs:47 src/main.rs:3232 25 | msgid "Error exporting to {}" 26 | msgstr "" 27 | 28 | #: src/printable.rs:332 src/printable.rs:703 src/main.rs:3199 29 | msgid "Error saving file {}" 30 | msgstr "" 31 | 32 | #: src/printable.rs:577 33 | msgid "Error saving page {}" 34 | msgstr "" 35 | 36 | #: src/printable.rs:1008 src/ui.rs:1229 37 | msgid "Page {}/{}" 38 | msgstr "" 39 | 40 | #: src/ui.rs:2685 41 | msgid "Created with Papercraft. https://github.com/rodrigorc/papercraft" 42 | msgstr "" 43 | 44 | #: src/main.rs:80 45 | msgid "Prevents editing of the model, useful as reference to build a real model" 46 | msgstr "" 47 | 48 | #: src/main.rs:120 src/main.rs:804 src/main.rs:2991 src/main.rs:3954 49 | msgid "Papercraft" 50 | msgstr "" 51 | 52 | #: src/main.rs:448 53 | msgid "Opening..." 54 | msgstr "" 55 | 56 | #: src/main.rs:449 57 | msgid "Saving..." 58 | msgstr "" 59 | 60 | #: src/main.rs:450 61 | msgid "Importing..." 62 | msgstr "" 63 | 64 | #: src/main.rs:451 65 | msgid "Updating..." 66 | msgstr "" 67 | 68 | #: src/main.rs:452 69 | msgid "Exporting..." 70 | msgstr "" 71 | 72 | #: src/main.rs:453 73 | msgid "Generating..." 74 | msgstr "" 75 | 76 | #: src/main.rs:757 77 | msgid "Error" 78 | msgstr "" 79 | 80 | #: src/main.rs:761 src/main.rs:1829 src/main.rs:2558 81 | msgid "OK" 82 | msgstr "" 83 | 84 | #: src/main.rs:775 src/main.rs:1837 src/main.rs:2551 85 | msgid "Cancel" 86 | msgstr "" 87 | 88 | #: src/main.rs:776 src/main.rs:2552 89 | msgid "Continue" 90 | msgstr "" 91 | 92 | #: src/main.rs:790 src/main.rs:2066 93 | msgid "About..." 94 | msgstr "" 95 | 96 | #: src/main.rs:811 97 | msgid "Version {}" 98 | msgstr "" 99 | 100 | #: src/main.rs:817 101 | msgid "Checking..." 102 | msgstr "" 103 | 104 | #: src/main.rs:823 105 | msgid "Check for new version" 106 | msgstr "" 107 | 108 | #: src/main.rs:849 109 | msgid "Already using latest version" 110 | msgstr "" 111 | 112 | #: src/main.rs:855 113 | msgid "New version {} available!" 114 | msgstr "" 115 | 116 | #: src/main.rs:918 117 | msgid "Please, wait..." 118 | msgstr "" 119 | 120 | #: src/main.rs:1101 121 | msgid "Face mode. Click to select a piece. Drag on paper to move it. Shift-drag on paper to rotate it." 122 | msgstr "" 123 | 124 | #: src/main.rs:1104 125 | msgid "Edge mode. Click on an edge to split/join pieces. Shift-click to join a full strip of quads." 126 | msgstr "" 127 | 128 | #: src/main.rs:1107 129 | msgid "Flap mode. Click on an edge to swap the side of a flap. Shift-click to hide a flap." 130 | msgstr "" 131 | 132 | #: src/main.rs:1110 133 | msgid "View mode. Click to highlight a piece. Move the mouse over an edge to highlight the matching pair." 134 | msgstr "" 135 | 136 | #: src/main.rs:1132 137 | msgid "Settings" 138 | msgstr "" 139 | 140 | #: src/main.rs:1141 141 | msgid "Language" 142 | msgstr "" 143 | 144 | #: src/main.rs:1153 145 | msgid "Theme" 146 | msgstr "" 147 | 148 | #: src/main.rs:1160 149 | msgctxt "Theme" 150 | msgid "Light" 151 | msgstr "" 152 | 153 | #: src/main.rs:1162 154 | msgctxt "Theme" 155 | msgid "Dark" 156 | msgstr "" 157 | 158 | #: src/main.rs:1192 src/main.rs:1943 159 | msgid "Document properties" 160 | msgstr "" 161 | 162 | #: src/main.rs:1246 163 | msgid "Number of pieces: {0}\n" 164 | "Number of flaps: {1}\n" 165 | "Real size (mm): {2}" 166 | msgstr "" 167 | 168 | #: src/main.rs:1264 169 | msgid "Model" 170 | msgstr "" 171 | 172 | #: src/main.rs:1270 173 | msgid "Scale" 174 | msgstr "" 175 | 176 | #: src/main.rs:1279 177 | msgid "Textured" 178 | msgstr "" 179 | 180 | #: src/main.rs:1282 181 | msgid "Texture filter" 182 | msgstr "" 183 | 184 | #: src/main.rs:1287 src/main.rs:1974 src/main.rs:2012 185 | msgid "Flaps" 186 | msgstr "" 187 | 188 | #: src/main.rs:1296 189 | msgctxt "FlapStyle" 190 | msgid "Textured" 191 | msgstr "" 192 | 193 | #: src/main.rs:1297 194 | msgctxt "FlapStyle" 195 | msgid "Half textured" 196 | msgstr "" 197 | 198 | #: src/main.rs:1298 199 | msgctxt "FlapStyle" 200 | msgid "White" 201 | msgstr "" 202 | 203 | #: src/main.rs:1299 204 | msgctxt "FlapStyle" 205 | msgid "None" 206 | msgstr "" 207 | 208 | #: src/main.rs:1304 src/main.rs:1382 209 | msgid "Style" 210 | msgstr "" 211 | 212 | #: src/main.rs:1318 213 | msgid "Shadow" 214 | msgstr "" 215 | 216 | #: src/main.rs:1331 217 | msgid "Double flap" 218 | msgstr "" 219 | 220 | #: src/main.rs:1336 src/main.rs:1568 221 | msgid "Width" 222 | msgstr "" 223 | 224 | #: src/main.rs:1337 src/main.rs:1403 src/main.rs:1422 src/main.rs:1452 src/main.rs:1469 src/main.rs:1569 src/main.rs:1580 src/main.rs:1776 src/main.rs:1785 src/main.rs:1794 src/main.rs:1803 225 | msgid "mm" 226 | msgstr "" 227 | 228 | #: src/main.rs:1351 229 | msgid "Angle" 230 | msgstr "" 231 | 232 | #: src/main.rs:1352 src/main.rs:1435 233 | msgid "deg" 234 | msgstr "" 235 | 236 | #: src/main.rs:1359 src/main.rs:1695 237 | msgid "Folds" 238 | msgstr "" 239 | 240 | #: src/main.rs:1370 241 | msgctxt "FoldStyle" 242 | msgid "Full line" 243 | msgstr "" 244 | 245 | #: src/main.rs:1372 246 | msgctxt "FoldStyle" 247 | msgid "Full & out segment" 248 | msgstr "" 249 | 250 | #: src/main.rs:1374 251 | msgctxt "FoldStyle" 252 | msgid "Out segment" 253 | msgstr "" 254 | 255 | #: src/main.rs:1375 256 | msgctxt "FoldStyle" 257 | msgid "In segment" 258 | msgstr "" 259 | 260 | #: src/main.rs:1376 261 | msgctxt "FoldStyle" 262 | msgid "Out & in segment" 263 | msgstr "" 264 | 265 | #: src/main.rs:1377 266 | msgctxt "FoldStyle" 267 | msgid "None" 268 | msgstr "" 269 | 270 | #: src/main.rs:1402 271 | msgid "Length" 272 | msgstr "" 273 | 274 | #: src/main.rs:1421 275 | msgid "Line" 276 | msgstr "" 277 | 278 | #: src/main.rs:1434 279 | msgid "Hidden fold angle" 280 | msgstr "" 281 | 282 | #: src/main.rs:1442 src/main.rs:1722 283 | msgid "Cuts" 284 | msgstr "" 285 | 286 | #: src/main.rs:1451 src/main.rs:1704 287 | msgid "Rims" 288 | msgstr "" 289 | 290 | #: src/main.rs:1468 291 | msgid "Tabs" 292 | msgstr "" 293 | 294 | #: src/main.rs:1478 295 | msgid "Information" 296 | msgstr "" 297 | 298 | #: src/main.rs:1483 299 | msgid "Layout" 300 | msgstr "" 301 | 302 | #: src/main.rs:1488 303 | msgid "Pages" 304 | msgstr "" 305 | 306 | #: src/main.rs:1499 307 | msgid "Columns" 308 | msgstr "" 309 | 310 | #: src/main.rs:1508 311 | msgid "Print Papercraft signature" 312 | msgstr "" 313 | 314 | #: src/main.rs:1515 315 | msgid "Print page number" 316 | msgstr "" 317 | 318 | #: src/main.rs:1526 319 | msgctxt "EdgeIdPos" 320 | msgid "None" 321 | msgstr "" 322 | 323 | #: src/main.rs:1527 324 | msgctxt "EdgeIdPos" 325 | msgid "Outside" 326 | msgstr "" 327 | 328 | #: src/main.rs:1528 329 | msgctxt "EdgeIdPos" 330 | msgid "Inside" 331 | msgstr "" 332 | 333 | #: src/main.rs:1532 334 | msgid "Edge id position" 335 | msgstr "" 336 | 337 | #: src/main.rs:1548 338 | msgid "Edge id font size" 339 | msgstr "" 340 | 341 | #: src/main.rs:1549 342 | msgid "pt" 343 | msgstr "" 344 | 345 | #: src/main.rs:1558 346 | msgid "Piece names only" 347 | msgstr "" 348 | 349 | #: src/main.rs:1562 350 | msgid "Paper size" 351 | msgstr "" 352 | 353 | #: src/main.rs:1579 354 | msgid "Height" 355 | msgstr "" 356 | 357 | #: src/main.rs:1591 358 | msgid "Resolution" 359 | msgstr "" 360 | 361 | #: src/main.rs:1592 362 | msgid "dpi" 363 | msgstr "" 364 | 365 | #: src/main.rs:1654 366 | msgid "Portrait" 367 | msgstr "" 368 | 369 | #: src/main.rs:1660 370 | msgid "Landscape" 371 | msgstr "" 372 | 373 | #: src/main.rs:1670 374 | msgid "User interface" 375 | msgstr "" 376 | 377 | #: src/main.rs:1678 378 | msgid "3D view" 379 | msgstr "" 380 | 381 | #: src/main.rs:1687 src/main.rs:1747 382 | msgid "Background" 383 | msgstr "" 384 | 385 | #: src/main.rs:1696 src/main.rs:1705 src/main.rs:1714 src/main.rs:1723 386 | msgid "px" 387 | msgstr "" 388 | 389 | #: src/main.rs:1713 390 | msgid "Rims with tab" 391 | msgstr "" 392 | 393 | #: src/main.rs:1738 394 | msgid "Paper view" 395 | msgstr "" 396 | 397 | #: src/main.rs:1755 src/main.rs:2042 398 | msgid "Paper" 399 | msgstr "" 400 | 401 | #: src/main.rs:1763 402 | msgid "Highlight" 403 | msgstr "" 404 | 405 | #: src/main.rs:1775 406 | msgid "top" 407 | msgstr "" 408 | 409 | #: src/main.rs:1784 410 | msgid "left" 411 | msgstr "" 412 | 413 | #: src/main.rs:1793 414 | msgid "right" 415 | msgstr "" 416 | 417 | #: src/main.rs:1802 418 | msgid "bottom" 419 | msgstr "" 420 | 421 | #: src/main.rs:1814 422 | msgid "Reset to UI defaults" 423 | msgstr "" 424 | 425 | #: src/main.rs:1845 426 | msgid "Apply" 427 | msgstr "" 428 | 429 | #: src/main.rs:1869 430 | msgid "File" 431 | msgstr "" 432 | 433 | #: src/main.rs:1871 src/main.rs:2345 434 | msgid "Open..." 435 | msgstr "" 436 | 437 | #: src/main.rs:1879 438 | msgid "Save" 439 | msgstr "" 440 | 441 | #: src/main.rs:1885 src/main.rs:2367 442 | msgid "Save as..." 443 | msgstr "" 444 | 445 | #: src/main.rs:1890 src/main.rs:2392 446 | msgid "Import model..." 447 | msgstr "" 448 | 449 | #: src/main.rs:1894 src/main.rs:2418 450 | msgid "Update with new model..." 451 | msgstr "" 452 | 453 | #: src/main.rs:1900 src/main.rs:2432 454 | msgid "Export model..." 455 | msgstr "" 456 | 457 | #: src/main.rs:1904 src/main.rs:2460 458 | msgid "Generate Printable..." 459 | msgstr "" 460 | 461 | #: src/main.rs:1911 462 | msgid "Settings..." 463 | msgstr "" 464 | 465 | #: src/main.rs:1922 466 | msgid "Quit" 467 | msgstr "" 468 | 469 | #: src/main.rs:1929 470 | msgid "Edit" 471 | msgstr "" 472 | 473 | #: src/main.rs:1932 474 | msgid "Undo" 475 | msgstr "" 476 | 477 | #: src/main.rs:1958 478 | msgid "Face/Island" 479 | msgstr "" 480 | 481 | #: src/main.rs:1966 482 | msgid "Split/Join edge" 483 | msgstr "" 484 | 485 | #: src/main.rs:1984 486 | msgid "Repack pieces" 487 | msgstr "" 488 | 489 | #: src/main.rs:1991 490 | msgid "View" 491 | msgstr "" 492 | 493 | #: src/main.rs:1993 494 | msgid "Textures" 495 | msgstr "" 496 | 497 | #: src/main.rs:2003 498 | msgid "3D lines" 499 | msgstr "" 500 | 501 | #: src/main.rs:2021 502 | msgid "X-ray selection" 503 | msgstr "" 504 | 505 | #: src/main.rs:2030 506 | msgid "Texts" 507 | msgstr "" 508 | 509 | #: src/main.rs:2051 510 | msgid "Highlight overlaps" 511 | msgstr "" 512 | 513 | #: src/main.rs:2059 514 | msgid "Reset views" 515 | msgstr "" 516 | 517 | #: src/main.rs:2064 518 | msgid "Help" 519 | msgstr "" 520 | 521 | #: src/main.rs:2332 522 | msgid "Load model" 523 | msgstr "" 524 | 525 | #: src/main.rs:2333 src/main.rs:2377 src/main.rs:3657 526 | msgid "The model has not been save, continue anyway?" 527 | msgstr "" 528 | 529 | #: src/main.rs:2376 530 | msgid "Import model" 531 | msgstr "" 532 | 533 | #: src/main.rs:2402 534 | msgid "Update model" 535 | msgstr "" 536 | 537 | #: src/main.rs:2403 538 | msgid "This model is not saved and this operation cannot be undone.\n" 539 | "Continue anyway?" 540 | msgstr "" 541 | 542 | #: src/main.rs:2549 543 | msgid "Overwrite?" 544 | msgstr "" 545 | 546 | #: src/main.rs:2550 547 | msgid "The file '{}' already exists!\n" 548 | "Would you like to overwrite it?" 549 | msgstr "" 550 | 551 | #: src/main.rs:2556 552 | msgid "Error!" 553 | msgstr "" 554 | 555 | #: src/main.rs:2557 556 | msgid "The file '{}' doesn't exist!" 557 | msgstr "" 558 | 559 | #: src/main.rs:3062 560 | msgid "Error opening file {}" 561 | msgstr "" 562 | 563 | #: src/main.rs:3065 564 | msgid "Error loading file {}" 565 | msgstr "" 566 | 567 | #: src/main.rs:3194 568 | msgid "Error creating file {}" 569 | msgstr "" 570 | 571 | #: src/main.rs:3656 572 | msgid "Quit?" 573 | msgstr "" 574 | 575 | #: src/main.rs:3962 576 | msgid "All models" 577 | msgstr "" 578 | 579 | #: src/main.rs:3976 580 | msgid "Wavefront" 581 | msgstr "" 582 | 583 | #: src/main.rs:3984 584 | msgid "Pepakura" 585 | msgstr "" 586 | 587 | #: src/main.rs:3992 588 | msgid "Stl" 589 | msgstr "" 590 | 591 | #: src/main.rs:4000 592 | msgid "glTF" 593 | msgstr "" 594 | 595 | #: src/main.rs:4011 596 | msgid "PDF documents" 597 | msgstr "" 598 | 599 | #: src/main.rs:4019 600 | msgid "SVG images" 601 | msgstr "" 602 | 603 | #: src/main.rs:4027 604 | msgid "Inkscape SVG multipage" 605 | msgstr "" 606 | 607 | #: src/main.rs:4035 608 | msgid "PNG images" 609 | msgstr "" 610 | 611 | #: src/main.rs:4043 612 | msgid "All files" 613 | msgstr "" 614 | -------------------------------------------------------------------------------- /src/paper/craft/update.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | fn compute_edge_map(new: &Papercraft, old: &Papercraft) -> FxHashMap { 4 | use rayon::prelude::*; 5 | 6 | let model = &new.model; 7 | let omodel = &old.model; 8 | let n_edges = model.num_edges(); 9 | 10 | // Brute force algorithm, probably it could be made much smarter, but it 11 | // is easier to invoke the Rayon superpowers. 12 | // It is not a function called so frequently. 13 | (0..n_edges) 14 | .into_par_iter() 15 | .map(EdgeIndex::from) 16 | .filter_map(|i_new| { 17 | let e_new = &model[i_new]; 18 | let (np0, np1) = model.edge_pos(e_new); 19 | let distance = |e_old: &Edge| { 20 | let (op0, op1) = omodel.edge_pos(e_old); 21 | let da = op0.distance2(np0) + op1.distance2(np1); 22 | let db = op0.distance2(np1) + op1.distance2(np0); 23 | (da, db) 24 | }; 25 | 26 | // f32 is not Eq so min_by_key cannot be used directly 27 | let best = omodel.edges().min_by(|&(_, e_old_1), &(_, e_old_2)| { 28 | let (da1, db1) = distance(e_old_1); 29 | let (da2, db2) = distance(e_old_2); 30 | let d1 = da1.min(db1); 31 | let d2 = da2.min(db2); 32 | d1.total_cmp(&d2) 33 | }); 34 | 35 | let (i_old, e_old) = best?; 36 | let (da, db) = distance(e_old); 37 | let crossed = da > db; 38 | Some((i_new, (i_old, crossed))) 39 | }) 40 | .collect() 41 | } 42 | 43 | type IslandFaceMap = FxHashMap>; 44 | 45 | fn compute_island_to_faces_map(pc: &Papercraft) -> IslandFaceMap { 46 | let mut new_faces_map = FxHashMap::default(); 47 | for (i_new, island) in pc.islands() { 48 | let mut faces = FxHashSet::default(); 49 | let _ = pc.traverse_faces_no_matrix(island, |f| { 50 | faces.insert(f); 51 | ControlFlow::Continue(()) 52 | }); 53 | new_faces_map.insert(i_new, faces); 54 | } 55 | new_faces_map 56 | } 57 | 58 | fn compute_island_map( 59 | new: &Papercraft, 60 | old: &Papercraft, 61 | new_map: &IslandFaceMap, 62 | old_map: &IslandFaceMap, 63 | ) -> FxHashMap { 64 | let mut map = FxHashMap::default(); 65 | for (i_island, _) in new.islands() { 66 | let new_faces = &new_map[&i_island]; 67 | let best = old.islands().max_by_key(|&(i_oisland, _)| { 68 | let old_faces = &old_map[&i_oisland]; 69 | new_faces.intersection(old_faces).count() 70 | }); 71 | if let Some((i_oisland, _)) = best { 72 | map.insert(i_island, i_oisland); 73 | } 74 | } 75 | map 76 | } 77 | 78 | impl Papercraft { 79 | pub fn update_from_obj(&mut self, old_obj: &Papercraft) { 80 | self.options = old_obj.options.clone(); 81 | // Options are changed, discard memo 82 | self.memo = Memoization::default(); 83 | 84 | // Check which edges are nearest, checking the distance between their vertices 85 | let eno_map = compute_edge_map(self, old_obj); 86 | let eon_map = compute_edge_map(old_obj, self); 87 | 88 | // If the best match for A is B and for B is A, then it is a match 89 | let mut real_edge_map = FxHashMap::default(); 90 | let mut edge_status_map = FxHashMap::default(); 91 | for (i_edge, _) in self.model.edges() { 92 | // If an edge matches in both directions, then it is a match 93 | let (o, o_cross) = match eno_map.get(&i_edge) { 94 | None => continue, 95 | Some(o) => *o, 96 | }; 97 | let (i, _) = match eon_map.get(&o) { 98 | None => continue, 99 | Some(i) => *i, 100 | }; 101 | if i_edge != i { 102 | continue; 103 | } 104 | real_edge_map.insert(i, o); 105 | 106 | let o_status = old_obj.edge_status(o); 107 | let i_status = self.edge_status(i); 108 | if i_status != EdgeStatus::Hidden && o_status != EdgeStatus::Hidden { 109 | edge_status_map.insert(i_edge, (o_status, o_cross)); 110 | } 111 | } 112 | 113 | //Apply the old status to the new model 114 | for (i_edge, (status, crossed)) in edge_status_map { 115 | // Is it a rim? 116 | if self.model[i_edge].faces().1.is_none() { 117 | // Rims can't be crossed, so ignore that 118 | self.edges[usize::from(i_edge)] = match status { 119 | EdgeStatus::Cut(FlapSide::Hidden) | EdgeStatus::Cut(FlapSide::False) => status, 120 | _ => EdgeStatus::Cut(FlapSide::Hidden), 121 | }; 122 | } else { 123 | match status { 124 | EdgeStatus::Hidden => { /* should not happen, because they are filtered above */ 125 | } 126 | EdgeStatus::Joined => { 127 | self.edge_join(i_edge, None); 128 | } 129 | EdgeStatus::Cut(c) => { 130 | self.edge_cut(i_edge, None); 131 | if let EdgeStatus::Cut(_) = self.edge_status(i_edge) { 132 | let c = if crossed { c.opposite() } else { c }; 133 | self.edges[usize::from(i_edge)] = EdgeStatus::Cut(c); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | self.memo = Memoization::default(); 141 | 142 | // Match the faces: two faces are equivalent if their 3 edges match 143 | let mut oi_real_face_map = FxHashMap::default(); 144 | let mut rotations = Vec::new(); 145 | for (i_face, face) in self.model.faces() { 146 | let i_edges = face.index_edges(); 147 | let o_edges = i_edges.map(|i| real_edge_map.get(&i)); 148 | let o_edges = match o_edges { 149 | [Some(a), Some(b), Some(c)] => [*a, *b, *c], 150 | _ => continue, 151 | }; 152 | 153 | // Instead of looking for the o_face everywhere, look just on the faces of one 154 | // of the o_edges, they are at most 2 faces. 155 | let o_faces = old_obj.model[o_edges[0]].faces(); 156 | let o_faces = std::iter::once(o_faces.0).chain(o_faces.1); 157 | //let o_edges_set = o_edges.into_iter().collect::>(); 158 | for o_face in o_faces { 159 | let oface = &old_obj.model[o_face]; 160 | let real_edges = oface.index_edges(); 161 | if real_edges == o_edges { 162 | //no rotation 163 | } else if real_edges[0] == o_edges[1] 164 | && real_edges[1] == o_edges[2] 165 | && real_edges[2] == o_edges[0] 166 | { 167 | rotations.push((i_face, -1)); 168 | } else if real_edges[0] == o_edges[2] 169 | && real_edges[1] == o_edges[0] 170 | && real_edges[2] == o_edges[1] 171 | { 172 | rotations.push((i_face, 1)); 173 | } else { 174 | // no match 175 | continue; 176 | }; 177 | // match! 178 | oi_real_face_map.insert(o_face, i_face); 179 | } 180 | } 181 | // Fix the order of edges inside the faces 182 | for (o_face, rotation) in rotations { 183 | self.model.rotate_face_vertices(o_face, rotation); 184 | } 185 | self.memo = Memoization::default(); 186 | 187 | // Match the islands: A maps to B if B is the target island with most common faces. 188 | let new_islands = compute_island_to_faces_map(self); 189 | let mut old_islands = compute_island_to_faces_map(old_obj); 190 | 191 | // old_islands uses FaceIndex from the old model, while new_islands uses FaceIndex from the new model, so they cannot compare directly. 192 | // Here we switch old_island to use the FaceIndex from the new model, if available. 193 | for (_, o_faces) in old_islands.iter_mut() { 194 | let i_faces = o_faces 195 | .iter() 196 | .filter_map(|o| oi_real_face_map.get(o)) 197 | .copied() 198 | .collect(); 199 | *o_faces = i_faces; 200 | } 201 | 202 | // If island A maps to B and B maps to A, then it is a match. 203 | let mut real_island_map = FxHashMap::default(); 204 | let ino_map = compute_island_map(self, old_obj, &new_islands, &old_islands); 205 | let ion_map = compute_island_map(old_obj, self, &old_islands, &new_islands); 206 | 207 | for (i_island, _) in self.islands() { 208 | let o = match ino_map.get(&i_island) { 209 | None => continue, 210 | Some(o) => *o, 211 | }; 212 | let i = match ion_map.get(&o) { 213 | None => continue, 214 | Some(i) => *i, 215 | }; 216 | if i != i_island { 217 | continue; 218 | } 219 | real_island_map.insert(i, o); 220 | } 221 | 222 | // Apply the same transformation to corresponding islands 223 | let mut new_island_pos: FxHashMap = 224 | self.islands.iter().map(|(i, _)| (i, None)).collect(); 225 | 226 | for (i, island) in &self.islands { 227 | let oisland = real_island_map 228 | .get(&i) 229 | .map(|o| old_obj.island_by_key(*o).unwrap()); 230 | 231 | // If the island is not matches, use any face that is matched as a second best option 232 | let oisland = oisland.or_else(|| { 233 | let new_faces = new_islands.get(&i).unwrap(); 234 | for i_f in new_faces { 235 | if let Some((o, _)) = oi_real_face_map.iter().find(|&(_, i)| i == i_f) { 236 | let oo = old_obj.island_by_face(*o); 237 | return old_obj.island_by_key(oo); 238 | } 239 | } 240 | None 241 | }); 242 | 243 | let Some(oisland) = oisland else { continue }; 244 | 245 | // If the root face has a direct map, use that; if not... 246 | let (iroot, rot, loc) = match oi_real_face_map.get(&oisland.root_face()) { 247 | Some(&i_f) if self.contains_face(island, i_f) => { 248 | (i_f, oisland.rotation(), oisland.location()) 249 | } 250 | _ => { 251 | // Look for any other face that is in both islands, and if found, 252 | // do some math to fit in the equivalent place 253 | let mut res = None; 254 | let _ = old_obj.traverse_faces(oisland, |o_f, _f, m| { 255 | match oi_real_face_map.get(&o_f) { 256 | Some(&i_f) if self.contains_face(island, i_f) => { 257 | res = Some((i_f, Rad::atan2(m.x.y, m.x.x), m.z.truncate())); 258 | ControlFlow::Break(()) 259 | } 260 | _ => ControlFlow::Continue(()), 261 | } 262 | }); 263 | if res.is_none() { 264 | println!("ffff"); 265 | } 266 | res.unwrap_or((island.root_face(), oisland.rotation(), oisland.location())) 267 | } 268 | }; 269 | new_island_pos.insert(i, Some((iroot, rot, loc))); 270 | } 271 | for (i_island, maybe_pos) in new_island_pos { 272 | let island = self.islands.get_mut(i_island).unwrap(); 273 | match maybe_pos { 274 | Some((iroot, rot, loc)) => { 275 | island.reset_transformation(iroot, rot, loc); 276 | } 277 | None => { 278 | // If the island doesn't have a mapping, dump it into the page -1. 279 | let mut page_offs = self.options.global_to_page(island.loc); 280 | page_offs.row = 0; 281 | page_offs.col = -1; 282 | let loc = self.options.page_to_global(page_offs); 283 | island.reset_transformation(island.root_face(), island.rotation(), loc); 284 | } 285 | } 286 | } 287 | self.memo = Memoization::default(); 288 | 289 | // Mixing two sane things may create something insane, fix it now 290 | self.sanitize(); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Papercraft 2 | 3 | ## Introduction 4 | 5 | Papercraft is a tool to unwrap paper 3D models, so that you can cut and glue them together and get a real world paper model. 6 | 7 | The main purpose of this program is to do the _unwrapping_, that is, it takes a 3D model as input and outputs a printable document with the pieces to cut. 8 | It is not a 3D modelling program. For that I recommend using [Blender][BLENDER]. 9 | 10 | The interface looks like this: 11 | 12 | ![UI](https://user-images.githubusercontent.com/1128630/212970567-75e869b7-7024-4d0c-95f7-58fd447fb67b.png) 13 | 14 | The printable document will look like this (just a piece): 15 | 16 | ![PDF](https://github.com/rodrigorc/papercraft/assets/1128630/9cef6e5e-da72-4713-985f-a419faa68f8d) 17 | 18 | 19 | And the final model is: 20 | 21 | 22 | 23 | Note: the model in the example is based on the [Low-Poly Pikachu][PIKACHU] by Agustin "Flowalistik" Arroyo, published under CC BY-NC-SA 4.0. 24 | You can get this Papercraft project from the [examples](./examples) directory, with the same license. 25 | 26 | ## License 27 | 28 | This program is published under the GPL-3.0-or-later license. See the [LICENSE][LIC] file for the full text. 29 | 30 | Files generated with this program are not affected by this license but by that of the 3D model you use, as it will be a derivative work. 31 | If in doubt contact a real lawyer. 32 | 33 | If you create any nice paper model, I'd appreciate if you open an [issue][ISSUE] and send some pictures. 34 | 35 | ## Installation 36 | 37 | If you use Windows, this program does not use any installation, you just copy it into a local folder and run it. Just download the latest package from [here][TAG] and uncompress it to your destination folder. 38 | 39 | Select the Win32 or Win64 version depending on your particular Windows and preferences. If you do not know which one you need, try both and see what happens. It requires a somewhat modern OpenGL implementation. If your graphics driver doesn't have one you'll get a blank screen, then you can try a software OpenGL driver, such as Mesa from [here][MESA_BIN]: uncompress the proper one to the same directory as Papercraft and it should just work. 40 | 41 | Then run the program directly by double-clicking or create a shortcut to your desktop. 42 | 43 | If you use Linux you can compile it from source (quite easy, really) or use the precompiled AppImage from the [releases][TAG] page. Remember to set the downloaded file to _executable_, then just run it. 44 | 45 | If you use any other OS you can try compiling from source, but I don't know if you will succeed. Let me now either way. 46 | 47 | ## Basic usage 48 | 49 | In the left side of the screen there is a 3D view of the model. In the right side there is the current 2D unwrapping. 50 | 51 | First of all you need a 3D model. Currently it only understands the Wavefront OBJ format, that is exportable by most (or all) usual 3D editing programs. Just remember 52 | that the material definition (textures) of an OBJ file is in a separate file with MTL extension. And the textures themselves, if any, are in separate image files, 53 | so if you move the model around remember to move all the dependent files too. 54 | 55 | When you load a model into Papercraft it will start with all faces split and distributed around the paper. Before startint cutting you should join them in big pieces and 56 | reorder them in the paper. 57 | 58 | For that the program has three basic working modes, selectables with the buttons in the toolbar or the _Edit_ menu: 59 | 60 | #### Edge mode 61 | 62 | This is probably the most important mode. If you click an open edge, faces to both sides of the edge will snap together. If you click on a closed edge it will split. 63 | If the Shift key is pressed while an open edge is clicked, then it will join a whole strip of faces, as long as they are composed of simple quads. This is useful for big meshes that are mostly made of quads and are usually unwrapped as parallel strips. 64 | 65 | With this tool you can create the pieces to be cut as big as you want. 66 | 67 | ### Flap mode 68 | 69 | In this mode, if you click an open edge the flap will switch to the other side. Press the Shift key while clicking to hide the flap. 70 | 71 | #### Face mode 72 | 73 | In this mode you can select and move the pieces in the 2D paper. Press the shift key while dragging to rotate the pieces. 74 | 75 | You can also move the pieces in any other mode, but this one disables clicking on edges, making the handling of smaller pieces much easier. 76 | 77 | ## Document properties 78 | 79 | In the menu Edit / Document properties you can edit many options related to the document as a whole: 80 | 81 | ![Document properties](https://github.com/rodrigorc/papercraft/assets/1128630/f60aa959-4f23-4383-a6d5-4bb1a4077212) 82 | 83 | * Model scale: the units in the original OBJ model are assumed to be millimeters. This value scales up or down that measure to make the final model bigger or smaller. 84 | You can check the final size of the model in the information box below. 85 | * Textured: you can disable the texture images so that in the 2D output you get only the outline of the pieces. Useful if you intend to paint it by hand. 86 | * Texture filter: whether you want to apply a bilinear filter to the texture image. You can unset if you prefer a pixelated minecraft-ly look, particularly with very low-resolution textures. 87 | * Flap style: how are the flaps (those paper flaps here the glue goes) generated: 88 | * Textured: they get the texture of the neighboring face, so that small imperfections when gluing them together are not so noticeable. 89 | * Half-textured: as textured but fading to white at the far edge. It makes them more easy to see, particularly if there are many small faces. 90 | * White: flaps are colorless. 91 | * None: no flaps. How will you glue the pieces together? 92 | * Shadow flaps: If it is greater than 0, it indicated the darkness of the _shadow flaps_. These are painted over the opposite side of a flap, and are particularly useful if you glue the tabs on the outside of the model, instead of the traditional inside. 93 | * Flap width: the maximum with of the flap, in millimeters. The may be smaller if the neighbor face is smaller. 94 | * Flap angle: the angle of the sides of the flap, in degrees. 90 will make them rectangles. 45 is a more useful value. 95 | Real angles may be smaller depending on the shape of the neighbor face. 96 | * Fold style: how the fold lines that instruct you where to fold are drawn: 97 | * Full line: a full line is drawn. The line will be solid when it is a _mountain_, dashed when it is a _valley_. 98 | * Out segment: only a small line to the sides of each fold will be drawn, outside the piece itself. This is so in the final model no lines will be visible. 99 | If you do this, you probable will want to do a rough cut keeping these segemnts, then fold the model and finally do the real cut-out. 100 | * Full & out segment: A combination of _full line_ and _out segment_. 101 | * In segment: Like _out segment_ but inside the model, only two small lines are drawn to each side of the fold. 102 | * Out & in segment: A combination of _in_ and _out_ segments. 103 | * None: No fold line. How to fold them is up to you. You you use very thin paper you can even try to glue the model without folding. 104 | * Fold length: when using in or out segments, the length of the segment. 105 | * Line width: with of the folded printed lines, in millimeters. 106 | * Hidden fold angle: edges that separate faces with an angle below this one will not be drawn. It is 0 by default meaning that all edges will be printed. It affects only angles between faces of the model, it will never hide the fold line for a flap; if you want to hide those set the "Fold style" to "None" instead. 107 | * Edge id position: Complex models are difficult to build. In order to help the user, each edge to be glued can be annotated with an "edge id". Each edge id is composed by the opposite piece name (one or a few letters), a colon and the edge number. Edges with the same number are to be glued together. With this option you can choose if you want to print the edge ids and piece names outside of the model, to keep your texture untainted, or inside the model, to keep the ids even after you've cut the pieces out. Or you can choose to omit the ids and not to print them. 108 | * Piece names only: Some models have many small pieces. Here the edge ids are not very usefull because they overlap a lot. But some people like to have names in the pieces nonetheless, because it helps to keep track of the already cut pieces. 109 | * Pages: the number of pages of the output printable document. 110 | * Columns: how many columns are used to order the pages in the 2D view. It does not have any effect in the final printable file. 111 | * Print Papercraft signature. You can disable the signature that is printed in the printable document linking to this Internet page. 112 | It is useful if you want to let your friends know how you create all these awesome paper models. Or you can disable it to keep your secrets. 113 | * Print page number: big models can be a bit of a mess, adding the page numer may help in keeping order. 114 | * Paper size: the size of the paper, in millimeters. The most usual paper sizes are listed in the drop-down menu. 115 | * DPI: (dots-per-inch) the resolution of the final printable file. Usual values are 300 and 600. Higher values mean better resolution but bigger files. 116 | * Margins: the margins of the page, in millimeters. The margins are shown in the 2D view but have no effect in the final printable file. 117 | 118 | ## Other options 119 | 120 | There are a few other options in this program, available from the main menu. 121 | These options do the expected thing: 122 | * File/Open: opens an existing Papercraft file. This program uses the `.craft` extension. 123 | * File/Save: saves the current project as a `.craft` file. 124 | * File/Save as: saves the project with a different name. 125 | * File/Import model: creates a new project using by importing an existing 3D model. Currently Papercraft understands the following formats: 126 | * Wavefrom OBJ, with textures. 127 | * Pepakura PDO, with textures and piece unwrapping. 128 | * STL models, no textures here, just the geometry. 129 | * File/Quit: closes this program. 130 | * Edit/Undo: undoes the last action. 131 | * Edit/Document properties: opens the "Document properties" dialog. 132 | * Edit/Face,Edge,Flaps: switches to the given mode. 133 | * Edit/Repack pieces: If you have all the pieces overlapping each other, this option will tidy them up a bit. 134 | * View/Reset views: If you move the 2D or 3D view too much and you lose yourself, this option will get you back to the initial view. 135 | 136 | More interesting are the following: 137 | 138 | ### File/Update with new model 139 | 140 | If you are unwrapping a model and you realize that there is some part that you don't like, you have to go back to your 3D editing tool and re-export the model file. 141 | Then, do you have to start the Papercraft unwrapping from scratch? Of course not! Just use this option to update the current Papercraft projects with an updated 3D model. 142 | It will try to keep the current work as much as it can. 143 | 144 | ### File/Export OBJ 145 | 146 | Did you import an OBJ model into Papercraft and then lost the original model? No problem, you can re-export the OBJ with this option and then import it in your 3D model software. 147 | 148 | ### Generate Printable 149 | 150 | The main purpose of this program is to create a document with the 2D unwrapping of the 3D model. This option does it. Big models with big paper and high resolution may take a while, so be patient. 151 | 152 | There are currently four formats for the printable document: 153 | 154 | * PDF: default format. It will create a multi-page PDF. 155 | * PNG: it creates one PNG file per page, autonumbered. 156 | * SVG: it creates one SVG file per page, as SVG format does not support multiple pages. Each SVG will have several layers, the idea is that those layers may be used to feed an automatic cutting machine. 157 | * One visible layer with the image to be printed. 158 | * One hidden layer with the cuts. 159 | * One hidden layer with the folds: one sublayer with the _valleys_ another with the _mountains_. 160 | * Inkscape SVG multipage: Inkscape implements a SVG extension that does support multiple pages in the same file. If you open such a file in other SVG viewer you'll probably only see the first page. It has the same layers than the previous SVG format, but repeated for every page. 161 | 162 | ### View/Textures 163 | 164 | Hides/shows the texture from 3D and 2D views. It may be easier to see the geometry. It will not affect the printable document. 165 | 166 | ### View/3D lines 167 | 168 | Hides/shows the lines from the 3D view. It makes the model look nicer. 169 | 170 | ### View/Flaps 171 | 172 | Hides/shows the flaps in the 2D view. It may be easier to see the geometry, particularly with small pieces. It will not affect the printable document. 173 | 174 | ### View/X-ray selection 175 | 176 | Normally the selected face/piece is shown in the 3D view over the geometry, even if it is hidden behind something. That is to make it easier to find it, 177 | but sometimes it may get confusing. This option can be used to disable that behavior. 178 | 179 | ### View/Highlight overlaps 180 | 181 | When two pieces overlap, or even one piece overlaps with itself, it may not be obvious. This option will highlight in pink any overlapping pixels. 182 | It will also highlight pieces that are outside of the paper, although that is easier to see. 183 | 184 | Note that some overlapping in flaps is perfectly acceptable, you just cut through them, but overlapping in real faces is not nice. 185 | 186 | [LIC]: LICENSE 187 | [ISSUE]: https://github.com/rodrigorc/papercraft/issues 188 | [BLENDER]: https://www.blender.org/ 189 | [PIKACHU]: https://www.printables.com/model/243-low-poly-pikachu 190 | [TAG]: https://github.com/rodrigorc/papercraft/releases/latest 191 | [MESA_BIN]: https://github.com/mmozeiko/build-mesa/releases 192 | -------------------------------------------------------------------------------- /locales/es.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Report-Msgid-Bugs-To: \n" 4 | "POT-Creation-Date: 2025-12-01 20:12+0000\n" 5 | "Last-Translator: \n" 6 | "Language: es\n" 7 | "Content-Type: text/plain; charset=UTF-8\n" 8 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 9 | 10 | #: src/printable.rs:42 11 | msgid "Don't know how to write the format of {}" 12 | msgstr "No sé cómo escribir el formato de {}" 13 | 14 | #: src/printable.rs:47 src/main.rs:3232 15 | msgid "Error exporting to {}" 16 | msgstr "Error exportando a {}" 17 | 18 | #: src/printable.rs:332 src/printable.rs:703 src/main.rs:3199 19 | msgid "Error saving file {}" 20 | msgstr "Error guardando archivo {}" 21 | 22 | #: src/printable.rs:577 23 | msgid "Error saving page {}" 24 | msgstr "Error guardando página {}" 25 | 26 | #: src/printable.rs:1008 src/ui.rs:1229 27 | msgid "Page {}/{}" 28 | msgstr "Pág. {}/{}" 29 | 30 | #: src/ui.rs:2685 31 | msgid "Created with Papercraft. https://github.com/rodrigorc/papercraft" 32 | msgstr "Creado con Papercraft. https://github.com/rodrigorc/papercraft" 33 | 34 | #: src/main.rs:80 35 | msgid "" 36 | "Prevents editing of the model, useful as reference to build a real model" 37 | msgstr "" 38 | "Evita editar el modelo, útil como referencia para construir el modelo real" 39 | 40 | #: src/main.rs:120 src/main.rs:804 src/main.rs:2991 src/main.rs:3954 41 | msgid "Papercraft" 42 | msgstr "Papercraft" 43 | 44 | #: src/main.rs:448 45 | msgid "Opening..." 46 | msgstr "Abriendo..." 47 | 48 | #: src/main.rs:449 49 | msgid "Saving..." 50 | msgstr "Guardando..." 51 | 52 | #: src/main.rs:450 53 | msgid "Importing..." 54 | msgstr "Importando..." 55 | 56 | #: src/main.rs:451 57 | msgid "Updating..." 58 | msgstr "Actualizando..." 59 | 60 | #: src/main.rs:452 61 | msgid "Exporting..." 62 | msgstr "Exportando..." 63 | 64 | #: src/main.rs:453 65 | msgid "Generating..." 66 | msgstr "Generando..." 67 | 68 | #: src/main.rs:757 69 | msgid "Error" 70 | msgstr "Error" 71 | 72 | #: src/main.rs:761 src/main.rs:1829 src/main.rs:2558 73 | msgid "OK" 74 | msgstr "Aceptar" 75 | 76 | #: src/main.rs:775 src/main.rs:1837 src/main.rs:2551 77 | msgid "Cancel" 78 | msgstr "Cancelar" 79 | 80 | #: src/main.rs:776 src/main.rs:2552 81 | msgid "Continue" 82 | msgstr "Continuar" 83 | 84 | #: src/main.rs:790 src/main.rs:2066 85 | msgid "About..." 86 | msgstr "Acerca de..." 87 | 88 | #: src/main.rs:811 89 | msgid "Version {}" 90 | msgstr "Versión {}" 91 | 92 | #: src/main.rs:817 93 | msgid "Checking..." 94 | msgstr "Comprobando..." 95 | 96 | #: src/main.rs:823 97 | msgid "Check for new version" 98 | msgstr "Comprobar nueva versión" 99 | 100 | #: src/main.rs:849 101 | msgid "Already using latest version" 102 | msgstr "Usando la última versión" 103 | 104 | #: src/main.rs:855 105 | msgid "New version {} available!" 106 | msgstr "¡Nueva versión {} disponible!" 107 | 108 | #: src/main.rs:918 109 | msgid "Please, wait..." 110 | msgstr "Por favor, espere..." 111 | 112 | #: src/main.rs:1101 113 | msgid "" 114 | "Face mode. Click to select a piece. Drag on paper to move it. Shift-drag on " 115 | "paper to rotate it." 116 | msgstr "" 117 | "Modo cara. Pincha para seleccionar una pieza. Arrastra para moverla, May-" 118 | "arrastre sobre el paper para rotarla." 119 | 120 | #: src/main.rs:1104 121 | msgid "" 122 | "Edge mode. Click on an edge to split/join pieces. Shift-click to join a full " 123 | "strip of quads." 124 | msgstr "" 125 | "Modo arista. Pincha en un arista para separar/unir piezas. May-pincha para " 126 | "unir una tira de cuads." 127 | 128 | #: src/main.rs:1107 129 | msgid "" 130 | "Flap mode. Click on an edge to swap the side of a flap. Shift-click to hide " 131 | "a flap." 132 | msgstr "" 133 | "Modo pestaña. Pincha en un arista para mover el lado de la pestaña. May-" 134 | "pincha para ocular la pestaña." 135 | 136 | #: src/main.rs:1110 137 | msgid "" 138 | "View mode. Click to highlight a piece. Move the mouse over an edge to " 139 | "highlight the matching pair." 140 | msgstr "" 141 | "Modo vista. Pincha para resaltar una pieza. Mueve el ratón sobre un arista " 142 | "para resaltar su complementario." 143 | 144 | #: src/main.rs:1132 145 | msgid "Settings" 146 | msgstr "Configuración" 147 | 148 | #: src/main.rs:1141 149 | msgid "Language" 150 | msgstr "Idioma" 151 | 152 | #: src/main.rs:1153 153 | msgid "Theme" 154 | msgstr "Tema" 155 | 156 | #: src/main.rs:1160 157 | msgctxt "Theme" 158 | msgid "Light" 159 | msgstr "Claro" 160 | 161 | #: src/main.rs:1162 162 | msgctxt "Theme" 163 | msgid "Dark" 164 | msgstr "Oscuro" 165 | 166 | #: src/main.rs:1192 src/main.rs:1943 167 | msgid "Document properties" 168 | msgstr "Propiedades del documento" 169 | 170 | #: src/main.rs:1246 171 | msgid "" 172 | "Number of pieces: {0}\n" 173 | "Number of flaps: {1}\n" 174 | "Real size (mm): {2}" 175 | msgstr "" 176 | "Número de piezas: {0}\n" 177 | "Número de pestañas: {1}\n" 178 | "Tamaño real (mm): {2}" 179 | 180 | #: src/main.rs:1264 181 | msgid "Model" 182 | msgstr "Modelo" 183 | 184 | #: src/main.rs:1270 185 | msgid "Scale" 186 | msgstr "Escala" 187 | 188 | #: src/main.rs:1279 189 | msgid "Textured" 190 | msgstr "Con textura" 191 | 192 | #: src/main.rs:1282 193 | msgid "Texture filter" 194 | msgstr "Filtro de textura" 195 | 196 | #: src/main.rs:1287 src/main.rs:1974 src/main.rs:2012 197 | msgid "Flaps" 198 | msgstr "Pestañas" 199 | 200 | #: src/main.rs:1296 201 | msgctxt "FlapStyle" 202 | msgid "Textured" 203 | msgstr "Con textura" 204 | 205 | #: src/main.rs:1297 206 | msgctxt "FlapStyle" 207 | msgid "Half textured" 208 | msgstr "Media textura" 209 | 210 | #: src/main.rs:1298 211 | msgctxt "FlapStyle" 212 | msgid "White" 213 | msgstr "Blanca" 214 | 215 | #: src/main.rs:1299 216 | msgctxt "FlapStyle" 217 | msgid "None" 218 | msgstr "Nada" 219 | 220 | #: src/main.rs:1304 src/main.rs:1382 221 | msgid "Style" 222 | msgstr "Estilo" 223 | 224 | #: src/main.rs:1318 225 | msgid "Shadow" 226 | msgstr "Sombra" 227 | 228 | #: src/main.rs:1331 229 | msgid "Double flap" 230 | msgstr "Doble solapa" 231 | 232 | #: src/main.rs:1336 src/main.rs:1568 233 | msgid "Width" 234 | msgstr "Ancho" 235 | 236 | #: src/main.rs:1337 src/main.rs:1403 src/main.rs:1422 src/main.rs:1452 237 | #: src/main.rs:1469 src/main.rs:1569 src/main.rs:1580 src/main.rs:1776 238 | #: src/main.rs:1785 src/main.rs:1794 src/main.rs:1803 239 | msgid "mm" 240 | msgstr "mm" 241 | 242 | #: src/main.rs:1351 243 | msgid "Angle" 244 | msgstr "Ángulo" 245 | 246 | #: src/main.rs:1352 src/main.rs:1435 247 | msgid "deg" 248 | msgstr "grados" 249 | 250 | #: src/main.rs:1359 src/main.rs:1695 251 | msgid "Folds" 252 | msgstr "Pliegues" 253 | 254 | #: src/main.rs:1370 255 | msgctxt "FoldStyle" 256 | msgid "Full line" 257 | msgstr "Línea completa" 258 | 259 | #: src/main.rs:1372 260 | msgctxt "FoldStyle" 261 | msgid "Full & out segment" 262 | msgstr "Línea y seg. exterior" 263 | 264 | #: src/main.rs:1374 265 | msgctxt "FoldStyle" 266 | msgid "Out segment" 267 | msgstr "Seg. exterior" 268 | 269 | #: src/main.rs:1375 270 | msgctxt "FoldStyle" 271 | msgid "In segment" 272 | msgstr "Seg. interior" 273 | 274 | #: src/main.rs:1376 275 | msgctxt "FoldStyle" 276 | msgid "Out & in segment" 277 | msgstr "Seg. interior y exterior" 278 | 279 | #: src/main.rs:1377 280 | msgctxt "FoldStyle" 281 | msgid "None" 282 | msgstr "Nada" 283 | 284 | #: src/main.rs:1402 285 | msgid "Length" 286 | msgstr "Longitud" 287 | 288 | #: src/main.rs:1421 289 | msgid "Line" 290 | msgstr "Línea" 291 | 292 | #: src/main.rs:1434 293 | msgid "Hidden fold angle" 294 | msgstr "Ángulo de pliegue oculto" 295 | 296 | #: src/main.rs:1442 src/main.rs:1722 297 | msgid "Cuts" 298 | msgstr "Cortes" 299 | 300 | #: src/main.rs:1451 src/main.rs:1704 301 | msgid "Rims" 302 | msgstr "Bordes" 303 | 304 | #: src/main.rs:1468 305 | msgid "Tabs" 306 | msgstr "Solapas" 307 | 308 | #: src/main.rs:1478 309 | msgid "Information" 310 | msgstr "Información" 311 | 312 | #: src/main.rs:1483 313 | msgid "Layout" 314 | msgstr "Disposición" 315 | 316 | #: src/main.rs:1488 317 | msgid "Pages" 318 | msgstr "Páginas" 319 | 320 | #: src/main.rs:1499 321 | msgid "Columns" 322 | msgstr "Columnas" 323 | 324 | #: src/main.rs:1508 325 | msgid "Print Papercraft signature" 326 | msgstr "Imprimir firma de Papercraft" 327 | 328 | #: src/main.rs:1515 329 | msgid "Print page number" 330 | msgstr "Imprimir número de página" 331 | 332 | #: src/main.rs:1526 333 | msgctxt "EdgeIdPos" 334 | msgid "None" 335 | msgstr "Nada" 336 | 337 | #: src/main.rs:1527 338 | msgctxt "EdgeIdPos" 339 | msgid "Outside" 340 | msgstr "Fuera" 341 | 342 | #: src/main.rs:1528 343 | msgctxt "EdgeIdPos" 344 | msgid "Inside" 345 | msgstr "Dentro" 346 | 347 | #: src/main.rs:1532 348 | msgid "Edge id position" 349 | msgstr "Posición id de arista" 350 | 351 | #: src/main.rs:1548 352 | msgid "Edge id font size" 353 | msgstr "Fuente id de arista" 354 | 355 | #: src/main.rs:1549 356 | msgid "pt" 357 | msgstr "pt" 358 | 359 | #: src/main.rs:1558 360 | msgid "Piece names only" 361 | msgstr "Solo nombre de piezas" 362 | 363 | #: src/main.rs:1562 364 | msgid "Paper size" 365 | msgstr "Tamaño de papel" 366 | 367 | #: src/main.rs:1579 368 | msgid "Height" 369 | msgstr "Alto" 370 | 371 | #: src/main.rs:1591 372 | msgid "Resolution" 373 | msgstr "Resolución" 374 | 375 | #: src/main.rs:1592 376 | msgid "dpi" 377 | msgstr "ppp" 378 | 379 | #: src/main.rs:1654 380 | msgid "Portrait" 381 | msgstr "Vertical" 382 | 383 | #: src/main.rs:1660 384 | msgid "Landscape" 385 | msgstr "Horizontal" 386 | 387 | #: src/main.rs:1670 388 | msgid "User interface" 389 | msgstr "Interfaz de usuario" 390 | 391 | #: src/main.rs:1678 392 | msgid "3D view" 393 | msgstr "Vista 3D" 394 | 395 | #: src/main.rs:1687 src/main.rs:1747 396 | msgid "Background" 397 | msgstr "Fondo" 398 | 399 | #: src/main.rs:1696 src/main.rs:1705 src/main.rs:1714 src/main.rs:1723 400 | msgid "px" 401 | msgstr "px" 402 | 403 | #: src/main.rs:1713 404 | msgid "Rims with tab" 405 | msgstr "Bordes con solapa" 406 | 407 | #: src/main.rs:1738 408 | msgid "Paper view" 409 | msgstr "Vista de papel" 410 | 411 | #: src/main.rs:1755 src/main.rs:2042 412 | msgid "Paper" 413 | msgstr "Papel" 414 | 415 | #: src/main.rs:1763 416 | msgid "Highlight" 417 | msgstr "Resalte" 418 | 419 | #: src/main.rs:1775 420 | msgid "top" 421 | msgstr "arriba" 422 | 423 | #: src/main.rs:1784 424 | msgid "left" 425 | msgstr "izquierda" 426 | 427 | #: src/main.rs:1793 428 | msgid "right" 429 | msgstr "derecha" 430 | 431 | #: src/main.rs:1802 432 | msgid "bottom" 433 | msgstr "abajo" 434 | 435 | #: src/main.rs:1814 436 | msgid "Reset to UI defaults" 437 | msgstr "Restaurar valores IU" 438 | 439 | #: src/main.rs:1845 440 | msgid "Apply" 441 | msgstr "Aplicar" 442 | 443 | #: src/main.rs:1869 444 | msgid "File" 445 | msgstr "Archivo" 446 | 447 | #: src/main.rs:1871 src/main.rs:2345 448 | msgid "Open..." 449 | msgstr "Abrir..." 450 | 451 | #: src/main.rs:1879 452 | msgid "Save" 453 | msgstr "Guardar" 454 | 455 | #: src/main.rs:1885 src/main.rs:2367 456 | msgid "Save as..." 457 | msgstr "Guardar como..." 458 | 459 | #: src/main.rs:1890 src/main.rs:2392 460 | msgid "Import model..." 461 | msgstr "Importar..." 462 | 463 | #: src/main.rs:1894 src/main.rs:2418 464 | msgid "Update with new model..." 465 | msgstr "Actualizar con nuevo modelo..." 466 | 467 | #: src/main.rs:1900 src/main.rs:2432 468 | msgid "Export model..." 469 | msgstr "Exportar modelo..." 470 | 471 | #: src/main.rs:1904 src/main.rs:2460 472 | msgid "Generate Printable..." 473 | msgstr "Generar impresión..." 474 | 475 | #: src/main.rs:1911 476 | msgid "Settings..." 477 | msgstr "Configuración..." 478 | 479 | #: src/main.rs:1922 480 | msgid "Quit" 481 | msgstr "Salir" 482 | 483 | #: src/main.rs:1929 484 | msgid "Edit" 485 | msgstr "Editar" 486 | 487 | #: src/main.rs:1932 488 | msgid "Undo" 489 | msgstr "Deshacer" 490 | 491 | #: src/main.rs:1958 492 | msgid "Face/Island" 493 | msgstr "Cara/isla" 494 | 495 | #: src/main.rs:1966 496 | msgid "Split/Join edge" 497 | msgstr "Separar/Unir arista" 498 | 499 | #: src/main.rs:1984 500 | msgid "Repack pieces" 501 | msgstr "Re-empaquetar piezas" 502 | 503 | #: src/main.rs:1991 504 | msgid "View" 505 | msgstr "Ver" 506 | 507 | #: src/main.rs:1993 508 | msgid "Textures" 509 | msgstr "Texturas" 510 | 511 | #: src/main.rs:2003 512 | msgid "3D lines" 513 | msgstr "Líneas 3D" 514 | 515 | #: src/main.rs:2021 516 | msgid "X-ray selection" 517 | msgstr "Selección rayos-X" 518 | 519 | #: src/main.rs:2030 520 | msgid "Texts" 521 | msgstr "Textos" 522 | 523 | #: src/main.rs:2051 524 | msgid "Highlight overlaps" 525 | msgstr "Resaltar solapamientos" 526 | 527 | #: src/main.rs:2059 528 | msgid "Reset views" 529 | msgstr "Reiniciar vistas" 530 | 531 | #: src/main.rs:2064 532 | msgid "Help" 533 | msgstr "Ayuda" 534 | 535 | #: src/main.rs:2332 536 | msgid "Load model" 537 | msgstr "Cargar modelo" 538 | 539 | #: src/main.rs:2333 src/main.rs:2377 src/main.rs:3657 540 | msgid "The model has not been save, continue anyway?" 541 | msgstr "El modelo no está guardado, ¿continuar igualmente?" 542 | 543 | #: src/main.rs:2376 544 | msgid "Import model" 545 | msgstr "Importar modelo" 546 | 547 | #: src/main.rs:2402 548 | msgid "Update model" 549 | msgstr "Actualizar modelo" 550 | 551 | #: src/main.rs:2403 552 | msgid "" 553 | "This model is not saved and this operation cannot be undone.\n" 554 | "Continue anyway?" 555 | msgstr "" 556 | "Este modelo no está guardado y esta operación no se puede deshacer.\n" 557 | "¿Continuar igualmente?" 558 | 559 | #: src/main.rs:2549 560 | msgid "Overwrite?" 561 | msgstr "¿Sobreescribir?" 562 | 563 | #: src/main.rs:2550 564 | msgid "" 565 | "The file '{}' already exists!\n" 566 | "Would you like to overwrite it?" 567 | msgstr "" 568 | "El archivo '{}' ya existe!\n" 569 | "¿Quieres sobreescribirlo?" 570 | 571 | #: src/main.rs:2556 572 | msgid "Error!" 573 | msgstr "Error!" 574 | 575 | #: src/main.rs:2557 576 | msgid "The file '{}' doesn't exist!" 577 | msgstr "¡El archivo '{}' no existe!" 578 | 579 | #: src/main.rs:3062 580 | msgid "Error opening file {}" 581 | msgstr "Error abriendo archivo {}" 582 | 583 | #: src/main.rs:3065 584 | msgid "Error loading file {}" 585 | msgstr "Error cargando archivo {}" 586 | 587 | #: src/main.rs:3194 588 | msgid "Error creating file {}" 589 | msgstr "Error creando archivo {}" 590 | 591 | #: src/main.rs:3656 592 | msgid "Quit?" 593 | msgstr "¿Salir?" 594 | 595 | #: src/main.rs:3962 596 | msgid "All models" 597 | msgstr "Todos los modelos" 598 | 599 | #: src/main.rs:3976 600 | msgid "Wavefront" 601 | msgstr "Wavefront" 602 | 603 | #: src/main.rs:3984 604 | msgid "Pepakura" 605 | msgstr "Pepakura" 606 | 607 | #: src/main.rs:3992 608 | msgid "Stl" 609 | msgstr "Stl" 610 | 611 | #: src/main.rs:4000 612 | msgid "glTF" 613 | msgstr "glTF" 614 | 615 | #: src/main.rs:4011 616 | msgid "PDF documents" 617 | msgstr "Documentos PDF" 618 | 619 | #: src/main.rs:4019 620 | msgid "SVG images" 621 | msgstr "Imágenes SVG" 622 | 623 | #: src/main.rs:4027 624 | msgid "Inkscape SVG multipage" 625 | msgstr "SVG Inkscape multipágina" 626 | 627 | #: src/main.rs:4035 628 | msgid "PNG images" 629 | msgstr "Imágenes PNG" 630 | 631 | #: src/main.rs:4043 632 | msgid "All files" 633 | msgstr "Todos los archivos" 634 | -------------------------------------------------------------------------------- /distro/macos/installer_background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 33 | 37 | 41 | 42 | 53 | 54 | 72 | 77 | 84 | 87 | 90 | 95 | 98 | 101 | 102 | 103 | Drag to install 115 | 116 | 276 | Papercraft 287 | 294 | 295 | 332 | 333 | -------------------------------------------------------------------------------- /src/paper/model/import/pepakura/data.rs: -------------------------------------------------------------------------------- 1 | // Pepakura PDO format is documented, by reverse engeneering, I presume, at: 2 | // * https://github.com/dpethes/pdo-tools.git/doc/pdo_spec_draft.txt 3 | // Many thanks to "dpethes" for the work! 4 | 5 | // I don't use many of the values, but I'll keep them there for reference 6 | #![allow(dead_code)] 7 | 8 | use std::cell::Cell; 9 | 10 | use super::super::*; 11 | use cgmath::Rad; 12 | 13 | #[derive(Debug)] 14 | pub struct Pdo { 15 | objs: Vec, 16 | mats: Vec, 17 | unfold: Option, 18 | settings: Settings, 19 | } 20 | 21 | impl Pdo { 22 | pub fn from_reader(mut rdr: R) -> Result { 23 | let mut reader = Reader { 24 | rdr: &mut rdr, 25 | version: 0, 26 | _mbcs: false, 27 | shift: 0, 28 | }; 29 | reader.read_pdo() 30 | } 31 | pub fn objects(&self) -> &[Object] { 32 | &self.objs 33 | } 34 | pub fn materials(&self) -> &[Material] { 35 | &self.mats 36 | } 37 | pub fn unfold(&self) -> Option<&Unfold> { 38 | self.unfold.as_ref() 39 | } 40 | pub fn settings(&self) -> &Settings { 41 | &self.settings 42 | } 43 | } 44 | 45 | struct Reader<'r, R> { 46 | rdr: &'r mut R, 47 | version: u8, 48 | _mbcs: bool, 49 | shift: u32, 50 | } 51 | 52 | #[derive(Debug)] 53 | pub struct BoundingBox { 54 | pub v0: Vector2, 55 | pub v1: Vector2, 56 | } 57 | 58 | #[derive(Debug)] 59 | pub struct Object { 60 | pub name: String, 61 | pub visible: bool, 62 | pub vertices: Vec, 63 | pub faces: Vec, 64 | pub edges: Vec, 65 | } 66 | 67 | #[derive(Debug)] 68 | pub struct Vertex { 69 | pub v: Vector3, 70 | } 71 | 72 | #[derive(Debug)] 73 | pub struct Face { 74 | pub mat_index: u32, 75 | pub part_index: u32, 76 | pub normal: Vector3, 77 | pub verts: Vec, 78 | } 79 | 80 | #[derive(Debug)] 81 | pub struct VertInFace { 82 | pub i_v: u32, 83 | pub pos2d: Vector2, 84 | pub uv: Vector2, 85 | pub flap: Option, 86 | } 87 | 88 | #[derive(Debug)] 89 | pub struct Flap { 90 | pub width: f32, 91 | pub angle1: Rad, 92 | pub angle2: Rad, 93 | } 94 | 95 | #[derive(Debug)] 96 | pub struct Edge { 97 | pub i_f1: u32, 98 | pub i_f2: Option, 99 | pub i_v1: u32, 100 | pub i_v2: u32, 101 | pub connected: bool, 102 | } 103 | 104 | #[derive(Debug)] 105 | pub struct Material { 106 | pub name: String, 107 | pub texture: Option, 108 | } 109 | 110 | pub struct Texture { 111 | pub width: u32, 112 | pub height: u32, 113 | pub data: Cell>, 114 | } 115 | impl std::fmt::Debug for Texture { 116 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 117 | fmt.debug_struct("Texture") 118 | .field("width", &self.width) 119 | .field("height", &self.height) 120 | .field("data", &"...") 121 | .finish() 122 | } 123 | } 124 | 125 | #[derive(Debug)] 126 | pub struct Unfold { 127 | pub scale: f32, 128 | pub padding: bool, 129 | pub bb: BoundingBox, 130 | pub parts: Vec, 131 | } 132 | 133 | #[derive(Debug)] 134 | pub struct Settings { 135 | pub margin_side: u32, 136 | pub margin_top: u32, 137 | pub page_size: Vector2, 138 | pub fold_line_hide_angle: Option, //degrees (180 = flat) 139 | } 140 | 141 | #[derive(Debug)] 142 | pub struct Part { 143 | pub i_obj: u32, 144 | pub bb: BoundingBox, 145 | pub name: String, 146 | pub lines: Vec, 147 | } 148 | 149 | #[derive(Debug)] 150 | pub struct Line { 151 | pub hidden: bool, 152 | pub type_: u32, //0: Cut, 1: mountain, 2: valley, >=3: invisible 153 | pub first: (u32, u32), // (i_face, i_vertex) 154 | pub second: Option<(u32, u32)>, 155 | } 156 | 157 | impl Reader<'_, R> { 158 | fn read_bounding_box(&mut self) -> Result { 159 | let v0 = read_vector2_f64(self.rdr)?; 160 | let v1 = read_vector2_f64(self.rdr)?; 161 | Ok(BoundingBox { v0, v1 }) 162 | } 163 | fn read_string(&mut self) -> Result { 164 | let len = read_u32(self.rdr)?; 165 | let mut res = vec![0; len as usize]; 166 | self.rdr.read_exact(&mut res)?; 167 | //TODO mbcs? 168 | res.pop(); 169 | for c in &mut res { 170 | *c = c.wrapping_sub(self.shift as u8); 171 | } 172 | Ok(String::from_utf8_lossy(&res).into_owned()) 173 | } 174 | fn read_texture(&mut self) -> Result { 175 | let width = read_u32(self.rdr)?; 176 | let height = read_u32(self.rdr)?; 177 | let size = read_u32(self.rdr)?; 178 | let mut zdata = vec![0; size as usize]; 179 | self.rdr.read_exact(&mut zdata)?; 180 | let mut z = flate2::bufread::ZlibDecoder::new(&zdata[..]); 181 | let mut data = Vec::with_capacity(width as usize * height as usize * 3); 182 | z.read_to_end(&mut data)?; 183 | Ok(Texture { 184 | width, 185 | height, 186 | data: Cell::new(data), 187 | }) 188 | } 189 | fn read_face(&mut self) -> Result { 190 | let mat_index = read_u32(self.rdr)?; 191 | let part_index = read_u32(self.rdr)?; 192 | let normal = read_vector3_f64(self.rdr)?; 193 | let _coord = read_f64(self.rdr)?; 194 | let n_verts = read_u32(self.rdr)?; 195 | let mut verts = Vec::with_capacity(n_verts as usize); 196 | for _ in 0..n_verts { 197 | let i_v = read_u32(self.rdr)?; 198 | let pos2d = read_vector2_f64(self.rdr)?; 199 | let uv = read_vector2_f64(self.rdr)?; 200 | let flap = read_bool(self.rdr)?; 201 | let width = read_f64(self.rdr)?; 202 | let a1 = read_f64(self.rdr)?; 203 | let a2 = read_f64(self.rdr)?; 204 | let flap = flap.then_some(Flap { 205 | width: width as f32, 206 | angle1: Rad(a1 as f32), 207 | angle2: Rad(a2 as f32), 208 | }); 209 | 210 | let mut _fold_info = [0; 24]; 211 | self.rdr.read_exact(&mut _fold_info)?; 212 | verts.push(VertInFace { 213 | i_v, 214 | pos2d, 215 | uv, 216 | flap, 217 | }); 218 | } 219 | Ok(Face { 220 | mat_index, 221 | part_index, 222 | normal, 223 | verts, 224 | }) 225 | } 226 | fn read_object(&mut self) -> Result { 227 | let name = self.read_string()?; 228 | let visible = read_bool(self.rdr)?; 229 | let n_vertices = read_u32(self.rdr)?; 230 | let mut vertices = Vec::with_capacity(n_vertices as usize); 231 | for _ in 0..n_vertices { 232 | let v = read_vector3_f64(self.rdr)?; 233 | vertices.push(Vertex { v }); 234 | } 235 | let n_faces = read_u32(self.rdr)?; 236 | let mut faces = Vec::with_capacity(n_faces as usize); 237 | for _ in 0..n_faces { 238 | let face = self.read_face()?; 239 | faces.push(face); 240 | } 241 | let n_edges = read_u32(self.rdr)?; 242 | let mut edges = Vec::with_capacity(n_edges as usize); 243 | for _ in 0..n_edges { 244 | let i_f1 = read_u32(self.rdr)?; 245 | let i_f2 = read_u32(self.rdr)?; 246 | let i_f2 = if i_f2 == u32::MAX { None } else { Some(i_f2) }; 247 | let i_v1 = read_u32(self.rdr)?; 248 | let i_v2 = read_u32(self.rdr)?; 249 | let connected = read_u16(self.rdr)? != 0; 250 | let _nf = read_u32(self.rdr)?; 251 | edges.push(Edge { 252 | i_f1, 253 | i_f2, 254 | i_v1, 255 | i_v2, 256 | connected, 257 | }); 258 | } 259 | Ok(Object { 260 | name, 261 | visible, 262 | vertices, 263 | faces, 264 | edges, 265 | }) 266 | } 267 | fn read_material(&mut self) -> Result { 268 | let name = self.read_string()?; 269 | for _ in 0..16 { 270 | let _color3d = read_f32(self.rdr)?; 271 | } 272 | for _ in 0..4 { 273 | let _color2d = read_f32(self.rdr)?; 274 | } 275 | let textured = read_bool(self.rdr)?; 276 | let texture = if textured { 277 | let tex = self.read_texture()?; 278 | Some(tex) 279 | } else { 280 | None 281 | }; 282 | Ok(Material { name, texture }) 283 | } 284 | fn read_part(&mut self) -> Result { 285 | let i_obj = read_u32(self.rdr)?; 286 | let bb = self.read_bounding_box()?; 287 | let name = if self.version >= 5 { 288 | self.read_string()? 289 | } else { 290 | String::new() 291 | }; 292 | let n_lines = read_u32(self.rdr)?; 293 | let mut lines = Vec::with_capacity(n_lines as usize); 294 | for _ in 0..n_lines { 295 | let line = self.read_line()?; 296 | lines.push(line); 297 | } 298 | Ok(Part { 299 | i_obj, 300 | bb, 301 | name, 302 | lines, 303 | }) 304 | } 305 | fn read_line(&mut self) -> Result { 306 | let hidden = read_bool(self.rdr)?; 307 | let type_ = read_u32(self.rdr)?; 308 | let _unk = read_u8(self.rdr)?; 309 | let i_f = read_u32(self.rdr)?; 310 | let i_v = read_u32(self.rdr)?; 311 | let first = (i_f, i_v); 312 | let second = read_bool(self.rdr)?; 313 | let second = if second { 314 | let face2_idx = read_u32(self.rdr)?; 315 | let vertex2_idx = read_u32(self.rdr)?; 316 | Some((face2_idx, vertex2_idx)) 317 | } else { 318 | None 319 | }; 320 | Ok(Line { 321 | hidden, 322 | type_, 323 | first, 324 | second, 325 | }) 326 | } 327 | fn read_pdo(&mut self) -> Result { 328 | const SIGNATURE: &[u8] = b"version 3\n"; 329 | for s in SIGNATURE { 330 | let c = read_u8(self.rdr)?; 331 | if c != *s { 332 | anyhow::bail!("signature error"); 333 | } 334 | } 335 | self.version = read_u32(self.rdr)? as u8; 336 | log::debug!("Version: {}", self.version); 337 | let mbcs = read_u32(self.rdr)?; 338 | log::debug!("MBCS: {mbcs}"); 339 | let _unk = read_u32(self.rdr)?; 340 | if self.version >= 5 { 341 | let designer = self.read_string()?; 342 | log::debug!("Designer: {designer}"); 343 | self.shift = read_u32(self.rdr)?; 344 | log::debug!("Shift: {}", self.shift); 345 | } 346 | let locale = self.read_string()?; 347 | log::debug!("Locale: {locale}"); 348 | let codepage = self.read_string()?; 349 | log::debug!("Codepage: {codepage}"); 350 | 351 | let texlock = read_u32(self.rdr)?; 352 | log::debug!("Texlock: {texlock}"); 353 | if self.version >= 6 { 354 | let show_startup_notes = read_bool(self.rdr)?; 355 | log::debug!("ShowStartupNotes: {show_startup_notes}"); 356 | let password_flag = read_bool(self.rdr)?; 357 | log::debug!("PasswordFlag: {password_flag}"); 358 | } 359 | let key = self.read_string()?; 360 | log::debug!("Key: {key}"); 361 | if self.version >= 6 { 362 | let v6_lock = read_u32(self.rdr)?; 363 | log::debug!("V6Lock: {v6_lock}"); 364 | for _ in 0..v6_lock { 365 | let _ = read_u64(self.rdr)?; 366 | } 367 | } else if self.version == 5 { 368 | let show_startup_notes = read_bool(self.rdr)?; 369 | log::debug!("ShowStartupNotes: {show_startup_notes}"); 370 | let password_flag = read_bool(self.rdr)?; 371 | log::debug!("PasswordFlags: {password_flag}"); 372 | } 373 | let assembled_height = read_f64(self.rdr)?; 374 | log::debug!("AssembledHeight: {assembled_height}"); 375 | let origin = read_vector3_f64(self.rdr)?; 376 | log::debug!("Origin: {origin:?}"); 377 | 378 | let n_objects = read_u32(self.rdr)?; 379 | let mut objs = Vec::with_capacity(n_objects as usize); 380 | for _ in 0..n_objects { 381 | let obj = self.read_object()?; 382 | objs.push(obj); 383 | } 384 | let n_materials = read_u32(self.rdr)?; 385 | let mut mats = Vec::with_capacity(n_materials as usize); 386 | for _ in 0..n_materials { 387 | let mat = self.read_material()?; 388 | mats.push(mat); 389 | } 390 | 391 | let has_unfold = read_bool(self.rdr)?; 392 | let unfold = if has_unfold { 393 | let scale = read_f64(self.rdr)? as f32; 394 | let padding = read_bool(self.rdr)?; 395 | let bb = self.read_bounding_box()?; 396 | let n_parts = read_u32(self.rdr)?; 397 | let mut parts = Vec::with_capacity(n_parts as usize); 398 | for _ in 0..n_parts { 399 | let part = self.read_part()?; 400 | parts.push(part); 401 | } 402 | let n_texts = read_u32(self.rdr)?; 403 | for _ in 0..n_texts { 404 | let _bb = self.read_bounding_box()?; 405 | let _line_spacing = read_f64(self.rdr)?; 406 | let _color = read_u32(self.rdr)?; 407 | let _font_size = read_u32(self.rdr)?; 408 | let _font_name = self.read_string()?; 409 | let n_lines = read_u32(self.rdr)?; 410 | for _ in 0..n_lines { 411 | let _text = self.read_string()?; 412 | } 413 | } 414 | let n_images = read_u32(self.rdr)?; 415 | for _ in 0..n_images { 416 | let _bb = self.read_bounding_box()?; 417 | let _tex = self.read_texture()?; 418 | } 419 | let n_images2 = read_u32(self.rdr)?; 420 | for _ in 0..n_images2 { 421 | let _bb = self.read_bounding_box()?; 422 | let _tex = self.read_texture()?; 423 | } 424 | Some(Unfold { 425 | scale, 426 | padding, 427 | bb, 428 | parts, 429 | }) 430 | } else { 431 | None 432 | }; 433 | if self.version >= 6 && unfold.as_ref().is_some_and(|u| !u.parts.is_empty()) { 434 | let n_unk = read_u32(self.rdr)?; 435 | for _ in 0..n_unk { 436 | let n_parts = read_u32(self.rdr)?; 437 | for _ in 0..n_parts { 438 | let _ = read_u32(self.rdr)?; 439 | } 440 | } 441 | } 442 | // settings 443 | let _show_flaps = read_bool(self.rdr)?; 444 | let _show_edge_id = read_bool(self.rdr)?; 445 | let _edge_id_pos = read_bool(self.rdr)?; 446 | let _face_mat = read_bool(self.rdr)?; 447 | let hide_almost_flat = read_bool(self.rdr)?; 448 | let fold_line_hide_angle = read_u32(self.rdr)?; 449 | let _draw_white_dot = read_bool(self.rdr)?; 450 | for _ in 0..4 { 451 | let _mountain_style = read_u32(self.rdr)?; 452 | } 453 | let page_type = read_u32(self.rdr)?; 454 | let mut page_size = match page_type { 455 | 0 /*A4*/=> Vector2::new(210.0, 297.0), 456 | 1 /*A3*/=> Vector2::new(297.0, 420.0), 457 | 2 /*A2*/=> Vector2::new(420.0, 594.0), 458 | 3 /*A1*/=> Vector2::new(594.0, 841.0), 459 | 4 /*B5*/=> Vector2::new(176.0, 250.0), 460 | 5 /*B4*/=> Vector2::new(250.0, 353.0), 461 | 6 /*B3*/=> Vector2::new(353.0, 500.0), 462 | 7 /*B2*/=> Vector2::new(500.0, 707.0), 463 | 8 /*B1*/=> Vector2::new(707.0, 1000.0), 464 | 9 /*letter*/=> Vector2::new(215.9, 279.4), 465 | 10 /*legal*/=> Vector2::new(215.9, 355.6), 466 | 11 => { 467 | let width = read_f64(self.rdr)?; 468 | let height = read_f64(self.rdr)?; 469 | Vector2::new(width as f32, height as f32) 470 | } 471 | _ /* unk */=> Vector2::new(210.0, 297.0), 472 | }; 473 | let orientation = read_u32(self.rdr)?; 474 | if orientation != 0 && page_size.y > page_size.x { 475 | //landscape 476 | std::mem::swap(&mut page_size.x, &mut page_size.y); 477 | } 478 | let margin_side = read_u32(self.rdr)?; 479 | let margin_top = read_u32(self.rdr)?; 480 | for _ in 0..12 { 481 | let _fold_pattern = read_f64(self.rdr)?; 482 | } 483 | let _outline_padding = read_bool(self.rdr)?; 484 | let _scale_factor = read_f64(self.rdr)?; 485 | if self.version >= 5 { 486 | let _author = self.read_string()?; 487 | let _comment = self.read_string()?; 488 | } 489 | let settings = Settings { 490 | margin_side, 491 | margin_top, 492 | page_size, 493 | fold_line_hide_angle: hide_almost_flat.then_some(fold_line_hide_angle), 494 | }; 495 | // eof! 496 | let eof = read_u32(self.rdr)?; 497 | assert!(eof == 9999); 498 | Ok(Pdo { 499 | objs, 500 | mats, 501 | unfold, 502 | settings, 503 | }) 504 | } 505 | } 506 | --------------------------------------------------------------------------------