Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 100 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
.cache/ci/linux-amd64/target/debug/.fingerprint
.cache/ci/linux-amd64/target/.rustc_info.json
.cache/ci/linux-amd64/home/.npm
key: n3-v1-ubuntu24-linux-amd64-${{ steps.environment.outputs.image }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
key: n3-v1-ubuntu24-linux-amd64-${{ steps.environment.outputs.image }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'vendor/**') }}
- name: Check formatting, lint, tooling, and focused regressions
env:
N3_CI_FAILURE_ARTIFACTS: "1"
Expand Down Expand Up @@ -94,7 +94,7 @@ jobs:
.cache/ci/linux-amd64/target/debug/.fingerprint
.cache/ci/linux-amd64/target/.rustc_info.json
.cache/ci/linux-amd64/home/.npm
key: n3-v1-ubuntu24-linux-amd64-${{ steps.environment.outputs.image }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
key: n3-v1-ubuntu24-linux-amd64-${{ steps.environment.outputs.image }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'vendor/**') }}
- name: Replay every guide feature and check exact artifacts
env:
N3_CI_FAILURE_ARTIFACTS: "1"
Expand Down Expand Up @@ -142,7 +142,7 @@ jobs:
target/debug/build
target/debug/.fingerprint
target/.rustc_info.json
key: n3-v1-macos15-${{ runner.os }}-${{ runner.arch }}-${{ steps.environment.outputs.platform }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.toml', 'Cargo.lock') }}
key: n3-v1-macos15-${{ runner.os }}-${{ runner.arch }}-${{ steps.environment.outputs.platform }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.toml', 'Cargo.lock', 'vendor/**') }}
- name: Restore the pinned just installation
id: just
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand Down Expand Up @@ -177,3 +177,100 @@ jobs:
~/.cargo/.crates.toml
~/.cargo/.crates2.json
key: ${{ steps.just.outputs.cache-primary-key }}

wasm:
name: WASM lint and browser build
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up the contributor Node.js version
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
package-manager-cache: false
- name: Identify the browser build runner
id: environment
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import hashlib
import os
image = os.environ.get("ImageVersion", "unreported")
print(f"platform={hashlib.sha256(image.encode()).hexdigest()}")
PY
- name: Restore WASM dependencies and build output
id: dependencies
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.cache/cargo/registry/cache
.cache/cargo/registry/index
target/debug/deps
target/debug/build
target/debug/.fingerprint
target/web/deps
target/web/build
target/web/.fingerprint
target/wasm32-unknown-unknown/debug/deps
target/wasm32-unknown-unknown/debug/build
target/wasm32-unknown-unknown/debug/.fingerprint
target/wasm32-unknown-unknown/web/deps
target/wasm32-unknown-unknown/web/build
target/wasm32-unknown-unknown/web/.fingerprint
target/.rustc_info.json
key: n3-v1-wasm-${{ runner.os }}-${{ runner.arch }}-${{ steps.environment.outputs.platform }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.toml', 'Cargo.lock', 'vendor/**') }}
- name: Restore pinned browser build tools
id: tools
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/just
~/.cargo/.crates.toml
~/.cargo/.crates2.json
.cache/web-tools/bin/wasm-bindgen
.cache/web-tools/.crates.toml
.cache/web-tools/.crates2.json
key: n3-v1-wasm-tools-just1.46.0-${{ runner.os }}-${{ runner.arch }}-${{ steps.environment.outputs.platform }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.toml') }}
- name: Install optional browser tooling using the repository Rust toolchain
run: |
cargo install just --version 1.46.0 --locked
just web-setup
# Compile and generate glue on every run. The native lanes continue to
# own behavioral tests and exact guide receipts; this job needs no GPU.
- name: Lint the WASM target and generate the browser application
run: just web-verify
- name: Save verified WASM dependencies and build output
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.dependencies.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.cache/cargo/registry/cache
.cache/cargo/registry/index
target/debug/deps
target/debug/build
target/debug/.fingerprint
target/web/deps
target/web/build
target/web/.fingerprint
target/wasm32-unknown-unknown/debug/deps
target/wasm32-unknown-unknown/debug/build
target/wasm32-unknown-unknown/debug/.fingerprint
target/wasm32-unknown-unknown/web/deps
target/wasm32-unknown-unknown/web/build
target/wasm32-unknown-unknown/web/.fingerprint
target/.rustc_info.json
key: ${{ steps.dependencies.outputs.cache-primary-key }}
- name: Save verified browser build tools
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.tools.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/just
~/.cargo/.crates.toml
~/.cargo/.crates2.json
.cache/web-tools/bin/wasm-bindgen
.cache/web-tools/.crates.toml
.cache/web-tools/.crates2.json
key: ${{ steps.tools.outputs.cache-primary-key }}
1 change: 1 addition & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"/Cargo.lock",
"/docs/guide/**",
"/docs/preview/vendor/**",
"/vendor/**",
"/examples/**",
"/fixtures/gltf/*/**"
]
Expand Down
14 changes: 12 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ decision.

## Product and interaction principles

- Focus on hard-surface mesh modeling. Broader modeling tools, web support, and
speculative UX ideas are separate work; do not silently expand a task to add them.
- Focus on hard-surface mesh modeling. Broader modeling tools and speculative
UX ideas are separate work; do not silently expand a task to add them.
- Keep actions semantic and key bindings separate. UI controls, shortcuts, and
tutorial replay should reuse the same behavior rather than implement variants.
- Respect input ownership: text fields, popups, active gestures, and edit sessions
Expand Down Expand Up @@ -73,6 +73,12 @@ Keep one application crate until a real consumer and a clear dependency boundary
justify extraction. Prefer focused modules over speculative crates, public APIs,
or generic frameworks.

Read [platform boundaries and performance](docs/architecture/platform-boundaries.md)
before changing host adapters or platform-dependent behavior. Share product
semantics, keep target selection at module boundaries, and preserve each host's
execution policy. Browser constraints must not lower the primary native target's
capabilities, performance policy, or verification baseline.

| Location | Responsibility |
| -------------------- | --------------------------------------------------------------------- |
| `src/model/` | Authored documents, units, native text codec, geometry evaluation |
Expand Down Expand Up @@ -108,6 +114,10 @@ The user guide is a primary development and review contract: maintainers give
feedback on the same narrative and evidence that real users read. Important
promises must be executable and fail visibly when they drift.

Native and web share one feature UI and user guide. Follow the
[shared product contract](docs/architecture/platform-boundaries.md#one-product-experience)
for capability-based availability and the boundary with contributor documentation.

Every guide feature owns one Rust scenario in `src/documentation/scenarios/` and
one template in `docs/templates/`, registered in
`src/documentation/features.rs`. Edit those sources; never hand-edit generated
Expand Down
18 changes: 16 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 56 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ edition = "2024"
rust-version = "1.95"
publish = false

[lib]
crate-type = ["rlib", "cdylib"]
# This private application library owns the existing single test inventory.
doctest = false

[[bin]]
name = "n3"
path = "src/main.rs"
test = false

[dependencies]
alacritty_terminal = "=0.26.0"
base64 = "0.22.1"
bytemuck = { version = "1.25.2", features = ["derive"] }
earcutr = "0.5.0"
egui = "=0.36.2"
egui-wgpu = "=0.36.2"
egui-winit = { version = "=0.36.2", default-features = false, features = [
"clipboard",
"links",
] }
glam = "0.33.11"
gltf = { version = "1.4.1", default-features = false, features = [
"utils",
Expand All @@ -35,21 +36,68 @@ image = { version = "=0.25.10", default-features = false, features = [
"png",
"webp",
] }
libc = "0.2"
pollster = "1.0.1"
percent-encoding = "2.3.2"
rfd = "0.17.2"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["float_roundtrip", "raw_value"] }
sha2 = "0.10.9"
wgpu = "30.0.1"
winit = "=0.30.13"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
alacritty_terminal = "=0.26.0"
egui-winit = { version = "=0.36.2", default-features = false, features = [
"clipboard",
"links",
] }
libc = "0.2"
pollster = "1.0.1"
rfd = "0.17.2"

[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
egui-winit = { version = "=0.36.2", default-features = false, features = [
"links",
] }
js-sys = "=0.3.106"
wasm-bindgen = "=0.2.129"
wasm-bindgen-futures = "=0.4.79"
web-time = "1.1.0"
web-sys = { version = "=0.3.106", features = [
"AddEventListenerOptions",
"console",
"CustomEvent",
"CustomEventInit",
"Document",
"DomRect",
"Element",
"Event",
"EventTarget",
"HtmlCanvasElement",
"HtmlElement",
"Navigator",
"Performance",
"Storage",
"StorageEvent",
"WheelEvent",
"Window",
] }

[dev-dependencies]
tobj = { version = "4.0.5", features = ["use_f64"] }

# egui-winit 0.36.2 implements only the native DroppedFile trait method.
# Preserve its input integration with a narrow WASM compatibility patch.
[patch.crates-io]
egui-winit = { path = "vendor/egui-winit" }

[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 2

# Keep local browser iteration quick without shipping Rust debug sections.
[profile.web]
inherits = "dev"
debug = false
strip = "debuginfo"
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,29 @@ Read the illustrated guide in your browser with `just docs` (requires Python 3).
It opens a local preview of the checked-in documentation; press Ctrl-C in the
terminal to stop the server.

### Browser baseline

The same Rust editor and viewport also run in a browser through WebAssembly and
WebGPU. To build and serve the local baseline:

```sh
just web-setup
just web
```

Use a desktop browser with WebGPU available. Native and web use the same
workspace and [user guide](docs/guide/README.md); unsupported host features are
omitted. See [development](docs/development.md#browser-build) for setup,
embedding, and platform limits.

## Current scope

N3 currently supports object transforms, vertex editing,
[making a face](docs/guide/make-face.md) from a triangle or a single flat outline,
and explicit [X-ray selection](docs/guide/xray.md). Broader topology tools,
edge/face selection, geometry snapping, and sculpting are future work.
Web support is also deferred.
The browser build is a technical baseline; full browser and device coverage is
still under evaluation.

OBJ import focuses on vertex positions and polygon faces. Materials, textures,
UVs, and authored normals are not preserved, and OBJ export is not implemented.
Expand Down
4 changes: 3 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ and document the implemented behavior in the executable user guide.
| Inspector number overflow | Keep each numeric field frame fixed while clipping or scrolling only its value text; egui's `DragValue` currently expands both. |
| Native accessibility | Wire egui's AccessKit adapter through the native event loop, add polite live announcements for toasts, and verify with VoiceOver. Keyboard focus and widget labels alone do not provide screen-reader announcements. |
| OS font support | Fonts as document materials; discovery/loading boundary suitable for a later web implementation. Separate from bundled interface fonts. |
| Initial WASM spike | Prove a minimal browser build and identify platform dependencies before deciding on a full port. |
| Web platform follow-up | Build on the [WASM browser baseline](docs/development.md#browser-build): browser/device coverage, touch/IME, external asset resource workflows, persistence/recovery, and multiple embedded workspaces. |
| Native/browser viewport profiling | [Issue #2](https://github.com/gridaco/n3/issues/2): reproduce large-mesh orbit stutter with comparable builds, real browser runs, and stage-level timings. Distinguish shared costs, web-host mistakes, and measured platform constraints before optimizing; follow the WASM baseline delivery. |
| Linux capture test concurrency | Diagnose allocator aborts and segmentation faults in parallel lavapipe tests, reproduced on the native baseline. The [opt-in CI runner](docs/development.md#verification-and-git-hooks) uses one test thread while retaining every test and exact capture; establish the cause before restoring parallel execution. |
| Exact macOS 27 guide frame | Replace the approximate static window chrome with measurements from a native macOS 27 N3 capture or Apple's design kit. Evaluate the translucent traffic-light material, highlights, and shadow only if exact mirroring is worth the rendering complexity. |

Existing context: [UX ideas](docs/research/ux-ideas.md),
Expand Down
Loading
Loading