diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df71d80..69feb12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" @@ -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" @@ -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 @@ -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 }} diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 3ae353a..ff5703c 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -7,6 +7,7 @@ "/Cargo.lock", "/docs/guide/**", "/docs/preview/vendor/**", + "/vendor/**", "/examples/**", "/fixtures/gltf/*/**" ] diff --git a/AGENTS.md b/AGENTS.md index 29aefa3..5f20b66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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 | @@ -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 diff --git a/Cargo.lock b/Cargo.lock index d5e1fa3..27c2ae7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,6 +409,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -625,8 +635,6 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc" dependencies = [ "arboard", "bytemuck", @@ -1485,6 +1493,7 @@ dependencies = [ "alacritty_terminal", "base64", "bytemuck", + "console_error_panic_hook", "earcutr", "egui", "egui-wgpu", @@ -1492,6 +1501,7 @@ dependencies = [ "glam", "gltf", "image", + "js-sys", "libc", "percent-encoding", "pollster 1.0.1", @@ -1500,6 +1510,10 @@ dependencies = [ "serde_json", "sha2", "tobj", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", "wgpu", "winit", ] diff --git a/Cargo.toml b/Cargo.toml index ffd3975..3371eff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", @@ -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" diff --git a/README.md b/README.md index 3564e07..db43ab1 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODO.md b/TODO.md index fec2e9f..50dcdba 100644 --- a/TODO.md +++ b/TODO.md @@ -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), diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md index 691659c..e94b792 100644 --- a/docs/architecture/architecture.md +++ b/docs/architecture/architecture.md @@ -1,32 +1,43 @@ # Architecture -N3 is one Rust package with one native application and an executable documentation -harness. Module boundaries are useful now; separate crates would imply an API -contract that the editor does not yet have. The package is not a published SDK. +N3 is one Rust package with native and browser application hosts and an executable +documentation harness. Module boundaries are useful now; separate crates would +imply an API contract that the editor does not yet have. The package is not a +published SDK. ## Source layout | Territory | Responsibility | | -------------------- | ---------------------------------------------------------------------------------------- | | `src/model/` | Authored documents, geometry evaluation, units, validation, and the native text codec. | -| `src/asset_io/` | Format adapters, resource resolution, open dispatch, and native document persistence. | +| `src/asset_io/` | Shared format adapters and resource policy; filesystem operations in `native/`. | | `src/scene/` | Internal imported scene model, validation, hierarchy and animation evaluation. | | `src/scene_view.rs` | Asset inspection, playback clock and independent view state. | | `src/editor/` | Selection, editing sessions, history integration, transforms, and derived picking state. | | `src/input/` | Physical input identity, semantic shortcuts, ownership, and shared navigation routing. | -| `src/render/` | Camera projection, derived mesh data, GPU resources, and scene rendering. | +| `src/render/` | Camera projection, GPU resources, scene rendering, and shared workspace frame mechanics. | | `src/ui/` | egui panels and viewport controls, properties, gizmo, rulers, and feedback. | | `src/terminal/` | Terminal emulator session and its egui view, outside authored documents and history. | | `src/documentation/` | Executable scenarios, virtual input, annotations, captures, and media generation. | | `src/native.rs` | Window lifecycle, native events, dialogs, filesystem effects, and GPU presentation. | +| `src/web.rs` | Browser canvas lifecycle, event delivery, WebGPU presentation, and application bridge. | | `src/settings/` | Global preferences, validation, merging, and the host storage interface. | -| `src/main.rs` | Composition and application entry points. | +| `src/lib.rs` | Shared composition and target-specific application entry points. | +| `src/main.rs` | Native executable entry point. | +| `web/` | Framework-neutral browser wrapper, DOM file effects, and local demo page. | The composition root retains internal module aliases so implementation modules can refer to each other without a broad import rewrite. They remain private to the application crate. These aliases are not public kernel exports, and the directory layout alone does not enforce a strict dependency graph. +Native and browser hosts share `WorkspaceUi`, `WorkspaceRenderer`, semantic +commands, and navigation policy. Hosts own execution and external effects. +[Platform boundaries and performance](platform-boundaries.md) defines this +contract, including target selection, independent performance policies, and the +shared UI and guide. Build, embedding, and platform limits belong in the +[development guide](../development.md#browser-build). + [Actions and menus](actions-and-menus.md) describes shared command metadata, live availability, shortcut presentation, and menu authoring. @@ -225,7 +236,8 @@ Split only when a second host or independent headless consumer needs a stable pure model/editing interface. First separate evaluation output from render adapters, and selection/transaction operations from egui coordinates and paint. A crate split should enforce those verified seams, not merely move the existing -coupling across package boundaries. Browser delivery remains future work. +coupling across package boundaries. The browser host currently reuses the +application modules in the same package; it does not require a crate split. ## Evidence boundaries @@ -234,3 +246,5 @@ renderer. Deterministic replay proves application routing, state transitions, an rendered outcomes. It does not prove macOS gesture recognition, actual device feel, file-dialog behavior, or window-system delivery. Keep focused native/manual acceptance alongside headless assertions and visual review of generated media. +Browser build and runtime checks establish a separate host boundary and do not +replace the native guide baseline or physical browser/device testing. diff --git a/docs/architecture/documentation-pipeline.md b/docs/architecture/documentation-pipeline.md index 142f44f..0b99c61 100644 --- a/docs/architecture/documentation-pipeline.md +++ b/docs/architecture/documentation-pipeline.md @@ -77,7 +77,11 @@ separate from physical key events; a native key/text pair must not type twice. Native scroll, pinch, and twist arrive outside egui's normal pointer routing. [navigation_events](../../src/input/navigation_events.rs) represents those inputs with logical-point or wheel-line deltas, native pinch deltas, rotation degrees, modifiers, and scroll phases. -The native window and documentation session call the same navigation router. +The native window, browser host, and documentation session call the same +navigation router. The browser adapter converts DOM wheel units and cumulative +WebKit gestures before routing; the existing navigation scenario exercises those +normalized events through `Session::navigation` without a separate guide or +capture baseline. It respects pointer location, window focus, floating UI, open menus, a pending view pie, and active mouse gestures. Active editor pointer gestures and property edits exclude camera input; released transform previews allow navigation while diff --git a/docs/architecture/platform-boundaries.md b/docs/architecture/platform-boundaries.md new file mode 100644 index 0000000..39b6495 --- /dev/null +++ b/docs/architecture/platform-boundaries.md @@ -0,0 +1,126 @@ +# Platform boundaries and performance + +This is the engineering contract for N3's native and browser hosts. + +> Share product semantics. Let each host own execution and realize its +> capabilities fully. + +Native remains the primary target. Browser support must preserve native +capabilities, rendering quality, and performance policy. Each supported host +should use the execution model and capabilities appropriate to its environment; +the browser's restrictions do not establish a common performance ceiling. + +## Shared behavior and host ownership + +The same operation must retain its meaning across input sources and hosts. +Document validation, editing transactions, history, units, selection rules, and +navigation policy stay shared. An adapter translates host input or performs a +host effect; it does not create an independent editing path. + +| Concern | Shared responsibility | Host responsibility | +| ----------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| Editing | Document semantics, commands, sessions, validation, and history | Deliver input and execute requested external effects | +| Navigation | Camera behavior, sensitivity, and input ownership | Translate device events, coordinates, units, and available gesture phases | +| Rendering | Scene evaluation, revision caches, uploads, and frame composition | Adapter/device selection, GPU limits, surfaces, recovery, and presentation | +| Time | Camera transitions and playback behavior | Monotonic or explicit replay clocks, frame pacing, and event-loop wakeups | +| Assets | Format decoding, unit conversion, and resource accounting | File access, selected bytes, resource resolution, and save completion | +| Preferences | Typed values, validation, and merge semantics | Storage implementation, synchronization triggers, and persistence guarantees | + +For example, DOM pinch events enter the shared navigation router through the +[browser gesture adapter](../../src/web/gestures.rs). The adapter handles browser +event delivery and duplicate signals; viewport ownership and camera behavior +remain shared. Missing gesture phase information is an explicit host limitation, +not a reason to discard native phase or momentum handling. + +## Focused implementation boundaries + +Select target implementations at composition and module boundaries. Keep `cfg` +out of ordinary editing rules and feature UI. Prefer a capability value when the +same UI needs to express whether an operation is available. + +Extract an adapter around an actual environmental difference, and share code +when its behavior and ownership agree. Keep one application crate until a real +consumer justifies a separate API. A port does not require a universal host trait, +a public modeling kernel, or identical implementations of every service. + +Current boundaries illustrate this policy: + +- [`asset_io/native/`](../../src/asset_io/native/mod.rs) owns filesystem operations; + format parsers and retained-resource accounting remain portable. +- [`WorkspaceRenderer`](../../src/render/workspace.rs) shares candidate preparation, + cache synchronization, rollback, scene rendering, and egui composition. Hosts + supply their actual device and target and retain presentation policy. +- [Terminal implementations](../../src/terminal/mod.rs) and + [guide inspection hooks](../../src/ui/controls.rs) are selected as modules. +- The [JavaScript wrapper](../../web/n3.js) forwards semantic commands and exposes + application state. An embedding UX must reuse the editor's transactions and + history. The existing Editor is still internal and depends on egui. + +## Performance, clocks, and scheduling + +Preserve each host's resource limits, cache behavior, concurrency options, and +presentation choices when extracting shared code. A browser limit must not become +a hard-coded limit in a shared renderer merely to make the port compile. Shared +rendering uses the device's enabled capabilities and retains revision-based work: +selection changes must not cause unnecessary geometry evaluation or uploads. + +Hosts acquire monotonic time and decide when to wake or present. Shared behavior +consumes elapsed time or frame input and can request repaint deadlines; hosts +schedule their delivery. Deterministic guide replay supplies an explicit clock. +Do not spread host clock calls through editing semantics or make native scheduling +inherit browser timer constraints. The native host currently uses +`std::time::Instant`; the browser uses `web_time::Instant`. + +Storage scheduling follows the same ownership rule. Browser preferences react to +local changes, storage events, and focus recovery, with timers for failed-attempt +retries. Native preferences retain filesystem polling. Both wait for safe input +boundaries before merging. Storage guarantees may differ and must be stated +accurately; sharing a controller does not make browser writes atomic across tabs. + +Measure representative workloads before claiming a performance improvement. +Compilation and architectural similarity establish neither frame-time parity nor +the best achievable performance. Report the host, workload, and measured effect +alongside any remaining limits. + +## One product experience + +Keep one feature UI, semantic action catalog, and user guide. Each feature owns +one narrative, scenario, and generated page across hosts. Adapt availability by +hiding or disabling unsupported controls in that shared UI; preserve familiar +menu organization and supported behavior. + +Presentation can use host capabilities without changing feature semantics. For +example, a future native Preferences window could be a real OS window while the +browser retains the existing egui window. Native multi-window delivery remains +future work; this example does not declare it implemented. + +Keep build, embedding, and host limitations in the +[development guide](../development.md#browser-build). This contract owns the +engineering policy; user-facing guides explain the shared features without +linking back to engineering documents. + +## Verification and review + +Preserve the native verification path and existing guide baseline. A browser +failure must not be resolved by weakening native checks, lowering rendering +quality, skipping artifacts, or creating a separate browser edition of the guide. +Any intentional native baseline change needs its own reason and review. + +Use shared behavioral regressions and native `just verify` for implementation +changes, plus target compilation and host integration checks for the port. +Separate the evidence: compilation proves build compatibility; runtime checks +exercise event delivery, rendering, storage, and lifecycle; physical-device tests +establish gesture recognition and feel; measurements support performance claims. +Browser checks supplement the shared guide's executable contract. They do not +replace its exact artifact checks or prove untested browsers and devices. + +Review a boundary change with these questions: + +1. Who owns the behavior, and do all entry points reuse its semantics? +2. Does a restriction belong to one host, and is it confined to that adapter? +3. Does the abstraction preserve each host's capabilities, scheduling, and caches? +4. Does the shared UI express availability without splitting the feature or guide? +5. What verifies native behavior, the affected host, and any performance claim? + +The [architecture overview](architecture.md) maps the modules, and +[development](../development.md) owns the runnable verification commands. diff --git a/docs/architecture/user-settings.md b/docs/architecture/user-settings.md index d891794..8c61ae6 100644 --- a/docs/architecture/user-settings.md +++ b/docs/architecture/user-settings.md @@ -1,8 +1,9 @@ # Global user settings -Settings are independent of documents. A single user-level `settings.json` -supplies preferences for every document; there is no workspace, project lookup, -inheritance chain, or settings embedded in `.n3.json`. +Settings are independent of documents. Native persists preferences in one +user-level `settings.json`; the browser uses an origin-local store. Each store +supplies preferences for every document opened by its host. There is no +workspace or project lookup, inheritance chain, or settings embedded in `.n3.json`. ## Responsibilities @@ -13,20 +14,22 @@ inheritance chain, or settings embedded in `.n3.json`. distinct from malformed or inaccessible data. - `src/native/settings_store.rs` resolves the macOS user path and implements bounded reads, guarded writes, permissions, and temporary-file ownership. -- The native host schedules synchronization and handles opening the text file. +- Each application host schedules synchronization; native also handles opening + the text file. `src/ui/user_settings.rs` maps validated settings into existing runtime behavior; Preferences emits host requests and displays errors without performing I/O. -- The native host supplies the current OS appearance. The settings model keeps +- Each host supplies its current appearance. The settings model keeps the user's `System`, `Light`, or `Dark` choice distinct from the resolved appearance used by egui. Style colors and measurements live in `src/theme.rs`; they are not serialized as a general theme configuration. - Core tests and documentation replay use memory stores. Native storage tests use temporary directories. None of them reads or writes the user's preferences. -The interface follows the needs of this native feature. A different host can -provide another store and an appropriate way to edit its text without changing -the settings model or widgets. This is not a browser implementation, and it does -not claim that other native application services are already portable. +Both hosts use the shared controller and Preferences UI. Persistence guarantees +belong to each store; sharing the controller does not make their storage APIs +equivalent. The [browser host constraints](../development.md#browser-host-constraints) +describe its storage, synchronization, and omitted controls. Native file +guarantees are specified below. `src/theme.rs` follows [shadcn's semantic theme tokens](https://ui.shadcn.com/docs/theming#theme-tokens) for the interface: background, card, popover, primary, secondary, muted, accent, borders, focus rings, @@ -40,10 +43,11 @@ interface components. ## Synchronization and interaction -Startup reads the user's file or uses defaults if it is absent. Reading defaults -does not create a file. A UI change or the explicit Open settings.json action -creates it. The first write includes the complete known defaults; an existing -partial file keeps omitted defaults and unknown keys. +Startup reads the host's stored snapshot or uses defaults if it is absent. +Reading defaults does not create stored data. A UI change requests persistence; native's +explicit Open settings.json action also creates a missing file. The first write +includes the complete known defaults; an existing partial snapshot keeps omitted +defaults and unknown keys. UI changes apply immediately to the running editor. Persist them after an active gesture or text entry finishes, rather than writing on every intermediate drag @@ -55,7 +59,7 @@ The controller compares three values per known key: the last successfully applie snapshot, current local preferences, and freshly read external preferences. It merges changes to different keys, accepts identical concurrent changes, and rejects conflicting changes to the same key. Failure does not advance the -baseline or overwrite the file. Explicit Reload adopts valid file values and +baseline or overwrite stored data. Explicit Reload adopts valid stored values and discards pending local preference edits. Invalid input retains active preferences and exposes an actionable error. @@ -75,20 +79,23 @@ accent may adjust lightness for contrast in each appearance while preserving the saved RGB value. Applying either appearance preference changes only presentation, not the document or history. -## File contract +## JSON contract -The macOS adapter uses `~/Library/Application Support/N3/settings.json`. The -application does not search the current directory for settings. A plain UTF-8 -JSON object uses dotted keys. Unknown values are retained during UI writes; +A plain UTF-8 JSON object uses dotted keys. Unknown values are retained during UI writes; known keys are validated before application. Duplicate keys, invalid known types or values, and input over 1 MiB are rejected. Comments and trailing commas are not supported. The [generated guide](../guide/settings.md) contains defaults and allowed values sourced from the settings model. -Documentation replay uses an explicit Dark appearance for stable existing +Documentation replay uses an explicit Light appearance for stable existing images. The settings feature deliberately selects Light and Dark before its respective captures; tests use isolated stores, never the host's actual file. +## Native file persistence + +The macOS adapter uses `~/Library/Application Support/N3/settings.json`. The +application does not search the current directory for settings. + Writes use a sibling temporary file, preserve existing permissions, and replace the destination only after checking its expected bytes. Cooperating N3 writers serialize through a sidecar lock. Creating a missing file must not overwrite a diff --git a/docs/baselines/linux-vulkan-lavapipe.json b/docs/baselines/linux-vulkan-lavapipe.json index d447788..b9132e3 100644 --- a/docs/baselines/linux-vulkan-lavapipe.json +++ b/docs/baselines/linux-vulkan-lavapipe.json @@ -2,7 +2,7 @@ "version": 1, "renderer": "linux-vulkan-lavapipe", "canonical_renderer": "macos-metal", - "canonical_guide_sha256": "6dfc9b2fbe0188b70c03933d4db501f308d7ad40fa12db5c5bec00a3be01e421", + "canonical_guide_sha256": "cc7355f856d22adcee6f3d3eaa7e6bbac6c4e54a05efeebeeefb758f9fdcbc4a", "artifacts": [ { "path": "2d-ruler.md", @@ -586,13 +586,13 @@ }, { "path": "manifest.txt", - "bytes": 412206, - "sha256": "a687f5453f07a0cd9c1e8b80ee705efb991102b98a051b3c28ca9e684af10ec8" + "bytes": 414413, + "sha256": "3a32c409bb90f84ca365f7e9d001b4e8462d3aae0d11d56c0663f59e22cbf97a" }, { "path": "navigation.md", - "bytes": 4618, - "sha256": "bfda019826ab2e2d41d02c1de85433d8506970fa11dfa76299c0771f28fd58ca" + "bytes": 4699, + "sha256": "2d580ce151767d7e96e97c32b7475e96f1194e34304cd53a8ee5165d42fb2fbf" }, { "path": "numeric-transforms.md", @@ -626,8 +626,8 @@ }, { "path": "settings.md", - "bytes": 5129, - "sha256": "c4f97b286ced7b58aa2b308fd63d5dfadb06a9b3fc3f0042fff4c750d516816d" + "bytes": 5246, + "sha256": "373b6bb038c35ac502dfd28b68f04bf77d3f88e3565ab8ed11c6a3b948fa3e41" }, { "path": "shading.md", @@ -641,8 +641,8 @@ }, { "path": "terminal.md", - "bytes": 2192, - "sha256": "8c9508bd62bcc21de6d7c9038cd81330fe5bf0ccef94f82f4d2fafc30e76c856" + "bytes": 2265, + "sha256": "49692389ef06088589f38c0bd40a64d4fad30d85e9cf35793abcf9878e12adc1" }, { "path": "toasts.md", diff --git a/docs/development.md b/docs/development.md index 3a57033..325bcad 100644 --- a/docs/development.md +++ b/docs/development.md @@ -61,10 +61,11 @@ tests. `just ci` still runs the complete sequence in one container, and native `just verify`, `just test`, and the pre-push hook always retain the full suite. Hosted jobs cache dependency downloads and compiled build output under keys -that include their platform, pinned toolchain/environment, and Cargo manifests. +that include their platform, pinned toolchain/environment, Cargo manifests, and +vendored dependency source. Incremental compilation output, generated guide artifacts, renderer receipts, diagnostics, and account files are excluded. Pull requests can restore caches; -only successful checks and native jobs on pushes to `main` save them. The guide +only successful checks, native, and WASM jobs on pushes to `main` save them. The guide job restores the shared Ubuntu cache without publishing it. Cache hits still execute every check, including the exact guide replay. The macOS job also caches the pinned `just` installation while retaining its ordinary installation command. @@ -74,6 +75,214 @@ review files keep their host ownership. The runner mounts isolated passwd/group entries read-only from its cache; the terminal backend needs a resolvable account even when its controlled shell is explicitly configured. +## Browser build + +The browser target uses the same package with an additional WASM library entry +point. The native application, documentation renderer, terminal backend, and +filesystem host remain target-specific. Both hosts use the same workspace and +user guide; host capabilities omit or disable unsupported controls. +Read [platform boundaries and performance](architecture/platform-boundaries.md) +before extending the port or changing host ownership. + +```sh +just web-setup +just web-check +just web-verify +just web-build +just web +``` + +`web-setup` installs the `wasm32-unknown-unknown` target for the repository's Rust +toolchain and the matching `wasm-bindgen-cli` version, 0.2.129, under +`.cache/web-tools`. `web-check` checks the library for that target with the locked +dependency graph. `web-build` uses the `web` Cargo profile and writes the static +application to ignored `build/web/`, including `pkg/n3.js`, `pkg/n3_bg.wasm`, and +the bundled font licenses. +It requires no Node, bundler, React installation, Docker, or application server. + +`web-verify` runs the wrapper lifecycle regressions with Node.js 24, Clippy with +warnings denied for the WASM library, and the complete static-site build. A +dedicated Ubuntu WASM CI job installs the optional tooling and runs this gate; +it supplements the native jobs without requiring a browser GPU on that runner. + +The pinned `egui-winit` 0.36.2 adapter requires narrow WASM compatibility patches +for its dropped-file trait and browser OS modifier mapping. The maintained source, upstream +licenses, and exact change are recorded in +[vendor/egui-winit/N3-PATCH.md](../vendor/egui-winit/N3-PATCH.md). Native behavior +is preserved; browser file contents enter through N3's selected-byte adapter. + +`just web` builds and serves that directory on localhost port 8000, then opens +the browser. To select another port or leave browser opening to another tool: + +```sh +just web --no-open --port 8001 +``` + +Keep the server running while using the application; Ctrl-C stops it. A desktop +browser with WebGPU enabled and available is required. Remote hosting requires +HTTPS; opening the generated HTML as a local `file:` URL is not the supported +launch path. The output is static and can be copied to an appropriate host when +deployment is separately requested. + +Target compilation and browser runtime checks are separate from `just verify`. +The native verification command continues to check shared behavior and the +strict documentation baseline. Browser runtime evidence must additionally record +successful WebGPU initialization, rendering, interaction, file round-tripping, +and lifecycle behavior on the browser actually tested. These host checks use the +same feature contract; they do not create a second guide or media baseline. + +Include a fresh-load input check before switching focus away from the canvas or +using controls in an embedding page. Move the pointer into the viewport, use +Shift+I to open Insert, create an object, click empty space to deselect, and +double-click the object to enter edit mode. Check held Z and Space through the +shared shading and navigation behavior, then verify focus loss cancels held +input and returning to the canvas restores ordinary interaction. Repeat with +multiple objects to check selection changes. Using wrapper buttons first can +hide a startup focus bug by causing an extra blur/focus transition. + +The browser host initializes egui focus from the current window after asynchronous +WebGPU setup; the canvas may already have received its initial focus event before +the input adapter exists. Keep subsequent focus events ordered through the usual +adapter. The local server disables HTTP caching, but an already-open tab retains +its loaded WASM until reloaded after a rebuild. + +### Browser gestures + +The browser host captures wheel and WebKit gesture events on its canvas, then +passes them through [browser_navigation](../src/input/browser_navigation.rs) +into the shared [navigation router](../src/input/navigation_events.rs). The +router retains viewport, focus, popup, and active-edit ownership. Canvas capture +prevents the browser's default page zoom for an editor pinch; it does not install +page-wide gesture handlers. Ordinary scrolling over egui controls remains UI +input rather than camera input. + +Browsers encode trackpad pinch as a wheel event with `ctrlKey`, or as WebKit's +cumulative `scale` and `rotation`. The wheel flag identifies the zoom event; it +must not become a synthetic held keyboard Control modifier. A real Control-wheel +combination is indistinguishable from pinch and also zooms. WebKit samples become +relative zoom and rotation deltas; events from the same gesture must not also +reach the camera through winit's wheel path. + +Pixel deltas use CSS pixels converted to egui logical points, independently of +the display's backing-pixel ratio. Line deltas retain wheel-line units; page +deltas use the canvas's CSS height. Browsers provide no reliable wheel-device +identity: unmodified pixel scrolling uses the shared precise-scroll policy, +including when a high-resolution mouse produces it. The adapter does not infer +finger or momentum start/end phases from timing. WebKit exposes rotation; +Chromium's pinch-wheel events contain no twist information. + +The shared navigation scenario exercises these conversions and ownership rules. +DOM delivery, browser default prevention, and physical trackpad feel require +browser testing; native replay alone cannot establish them. + +### Embedding + +The framework-neutral [wrapper](../web/n3.js) exports +`mountN3(container, { onState, onError })`. It creates and sizes the canvas, awaits +GPU startup, and returns a controller: + +| Method | Behavior | +| ------------------------ | ------------------------------------------------------------------------------------------- | +| `command(id)` | Dispatch a semantic action, such as `insert.cube` or `history.undo`, then focus the canvas. | +| `open(file, { append })` | Read a browser `File` and confirm replacement when needed; `append` defaults to `false`. | +| `download(filename)` | Request a document download; the default name is `Untitled.n3.json`. | +| `documentJSON()` | Return serialized document text. | +| `snapshot()` | Return the parsed status object. | +| `destroy()` | Stop the editor, disconnect observation, and remove listeners and the canvas. | + +The controller exposes its `canvas`. `onState` receives status changes including +object and selection counts, dirty state, dimensions, and errors; callbacks run +after the Rust state borrow is released. A JavaScript or React application may +own surrounding UI while Rust retains workspace behavior and document editing. +This experimental bridge does not expose arbitrary document mutation, GPU +resources, or a stable UI-independent editor API. + +```js +import { mountN3 } from "/n3/n3.js"; + +const editor = await mountN3(document.querySelector("#editor"), { + onState: (state) => console.log(state.objects, state.dirty), + onError: (error) => console.error(error), +}); +``` + +Serve `n3.js` and its generated `pkg/` together under `/n3/` in this example. +Give the container an explicit usable size. Preserve the wrapper's file +confirmation and teardown behavior when extending it. + +The lower-level generated `start(canvas)` export returns a `WebApp` handle and +initializes the GPU asynchronously. The canvas emits `n3-ready` or `n3-error`; +methods require a ready application. Its methods are `command(id)`, +`load_file(name, bytes, append)`, `document_json()`, `new_document()`, +`settings_json()`, `snapshot()`, `resize(width, height)`, and `destroy()`. +Direct callers must resolve unsaved changes before replacing the document. + +Mounting is asynchronous: unmounting before startup completes must still destroy +the eventual controller. The baseline supports one editor lifetime per WASM +module; destroying it does not establish that the module can mount again. +Direct React effects that remount, including development StrictMode, therefore +require additional lifecycle work. Ordinary React mounting and removal can use +an iframe, which gives each frame its own module lifetime: + +```jsx +export function N3Panel() { + return ( +