Keep existing viewer URLs compatible and test embedded pages, assets, and isolation headers. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
51 lines
8.5 KiB
Markdown
51 lines
8.5 KiB
Markdown
# Yawn Sculpt
|
||
|
||
Open `/sculpt` on the Yawn server (`npm run server`); `/view/sculpt` remains a compatibility alias. The production Docker image embeds this example, so the existing main-branch deployment poller publishes it at `/sculpt` with each deployment. All example code lives here; the only integration outside this folder is the embedded-page route in `server/app.go`. Rendering imports **only `@yawn/core`**. No handles, third-party scene graph, or remote document storage.
|
||
|
||
## Workflow
|
||
|
||
Five brushes: Draw, Inflate, Smooth, Flatten, Grab. X symmetry merges reflected displacement without double deposition. Pen pressure modulates strength through `clamp(gain × pressure^gamma, 0, 1)`; pressure-radius is optional. Mouse/touch use constant pressure. Settings and temporary Shift-smooth / Alt-invert are captured at stroke start. Grab uses a fixed camera-facing drag plane and stroke-start positions.
|
||
|
||
The compact left icon rail contains brushes, brush settings, undo/redo and Settings. Hover or tap the settings icons to reveal their panels; tap inside to keep one open. Projects and palm rejection live in Settings, with less-used pen/display controls in disclosures. Panels overlay the canvas without resizing it and close on viewport resize. Orientation changes preserve apparent zoom, pan and rotation.
|
||
|
||
Drag the top-right XYZ gizmo to orbit, or its neighboring pan/zoom icons to move the view. Click an axis endpoint to align the camera; click the facing endpoint again to flip sides. The scene is Y-up. Navigation captures its pointer outside the controls and never sculpts; Escape, lost capture, blur and viewport changes restore the starting view. Navigation waits for an unfinished sculpt transaction to settle. Axis buttons and navigation controls also support keyboard use.
|
||
|
||
Controls use the locally vendored Basecoat 1.0.2 standalone stylesheet (shadcn-style native HTML components); provenance and license are in `vendor/basecoat/`. No React, UI JavaScript runtime, CDN, or additional build step. Exclude the unmodified vendor stylesheet from formatters. `styles.css` handles layout/density; `Menus` remains the visibility controller.
|
||
|
||
Palm rejection defaults on: canvas touch is ignored, including outside-menu dismissal, while touch controls remain usable. With rejection off, one finger sculpts and two fingers pan/pinch, rolling back any provisional one-finger sculpt. Right-drag rotates, middle-drag pans, and the wheel zooms. Escape, blur, canceled input and unexpected capture loss roll back the stroke. Undo/redo acts on whole strokes. Native `.yawn-sculpt` projects preserve mesh geometry and density (not history or settings); OBJ export is one-way. Download before leaving—there is no automatic persistence.
|
||
|
||
## Performance bounds and limitations
|
||
|
||
| New sphere | Vertices | Triangles | History cap |
|
||
| ---------- | -------: | --------: | ----------: |
|
||
| Simple | 2,562 | 5,120 | 8 MiB |
|
||
| Balanced | 10,242 | 20,480 | 16 MiB |
|
||
| Fine | 40,962 | 81,920 | 32 MiB |
|
||
|
||
History is capped by bytes and 50 entries, with a separate bounded active-stroke position snapshot. A refitted triangle BVH accelerates picking and brush neighborhoods. Affected normals include all incident faces at brush boundaries. Display quality changes pixel budgets (~0.6/1/2 MP) and frame caps (30/60/60), never mesh density. The 8 MiB shared-row arena is **not** the application's total memory budget.
|
||
|
||
Input occupies **one 64-byte `sculptSample` row in core's SAB**, not a sample queue. Main publishes the latest endpoint/pressure; subtracting the previously consumed endpoint gives the accumulated movement. Atomic version tokens protect a coherent snapshot and retire only the consumed update, leaving concurrent newer input pending. A dedicated worker samples this slot with its own `requestAnimationFrame` and performs brush raycasts, deformation, normals and BVH refits. It interpolates straight segments between consumed endpoints; this frame-level approximation does not retain intermediate bends or pressure changes. Grab separately preserves the first effective pressure that establishes its influence.
|
||
|
||
The worker retains at most one unfinished segment plus the latest shared endpoint. Resumable interpolation yields after a soft 4 ms budget or 64 brush attempts; an unfinished segment exceeding 250 ms cancels the provisional stroke. One synchronous dab can exceed the budget, especially for large brushes and Detail. Shared memory avoids an input message queue, not the CPU cost of sculpting. Worker and renderer animation frames are independently scheduled, not a same-frame presentation guarantee.
|
||
|
||
Worker geometry packets are immutable, transferred, and single-flight; main applies them to its document mirror and acknowledges them. GPU uploads retain their separate single-flight staging guard. Initial picking, document/history operations, and one face-normal/BVH maintenance pass at stroke completion remain on main. Completed geometry/history survive renderer or sculpt-worker failure; unfinished work rolls back. Resize recreates both workers and the shared row because core does not expose surface resizing. Viewport rectangle changes cancel unfinished strokes immediately; new strokes wait for cancellation acknowledgement and the matching renderer size. The sculpt worker stops its animation loop between strokes; core's renderer still has its own loop.
|
||
|
||
Requires WebGPU, OffscreenCanvas, shared memory, cross-origin isolation, and dedicated-worker animation frames. Observable initialization/upload failures preserve the document and expose Retry plus Save/Export; core does not expose comprehensive device-loss detection. A front-facing brush filter is not geodesic/occlusion isolation on folded surfaces. This is a fixed-topology, single-sphere sculpting studio inspired by Nomad, **not Nomad parity**: no remesh, dynamic topology, layers, painting, multi-object scenes or arbitrary mesh import. Coordinates are bounded to ±8 model units.
|
||
|
||
## Verification
|
||
|
||
```sh
|
||
node --test server/web/assets/sculpt/*.test.js
|
||
go test ./server
|
||
```
|
||
|
||
Tests exercise topology, brute-force picking comparisons, local normals, pressure, symmetry, history, projects, resumable sampling, input transactions and staged uploads. Benchmark output is orb CPU timing, not a low-end hardware or frame-rate guarantee. Physical pen feel, palm behavior and constrained-device performance require real-device testing.
|
||
|
||
In a fresh browser page, `await (await import('/assets/sculpt/latency-check.js')).check()` replays Draw and Grab at 1, 4, and 16 coalesced samples per animation frame on Balanced-density spheres. It verifies zero main-thread dabs and fixed shared-input storage, and reports main input/packet handling, uploads, release-to-Save-enabled delay and cancellations separately. This synthetic check does not measure physical pen-to-display latency. Unit tests include a real two-thread SAB stress test and deterministic delayed-publication/cancellation races.
|
||
|
||
For visual checks in the orb, use `agent-browser --headed --webgpu`: this environment's headless WebGPU presentation can be blank even when rendering succeeds. Inspect desktop, narrow portrait/landscape, settings open, error, and modified/undo/redo states. Test save/reopen and rejected files. Use the page's Help for all gestures and shortcuts.
|
||
|
||
Run `await (await import('/assets/sculpt/browser-check.js')).check()` on a fresh page for transaction and UI regressions. This harness stubs pointer capture; native acceptance must use a separate fresh page. Through the browser CDP endpoint (`agent-browser get cdp-url`), use `Input.dispatchMouseEvent` with `pointerType: 'pen'` to drag each navigation control beyond its bounds. Assert actual `hasPointerCapture(event.pointerId)`, release outside, and verify Escape restores all camera fields. During a visibly deforming stroke, focus Center view, send Escape with `Input.dispatchKeyEvent`, release the pen, and verify exact geometry and unchanged history after settlement.
|
||
|
||
For rail scrolling, enable `Emulation.setTouchEmulationEnabled` and set a 640×360 CSS-pixel viewport. Verify `(pointer: coarse)` and `scrollHeight > clientHeight`, then use `Input.dispatchTouchEvent` to swipe upward from the center of a brush button. Assert increasing rail `scrollTop`, unchanged selected brush and geometry, and successful tapping of Settings at the bottom. Temporarily overriding rail buttons to `touch-action: none` should reproduce zero scrolling. Do not substitute dispatched DOM pointer events, wheel scrolling, or assigning `scrollTop` for this native gesture check. Browser emulation is not physical-tablet verification.
|