Commit Graph
201 Commits
Author SHA1 Message Date
Sebastian ec96612388 fix: resolve the two clippy approx_constant denies
The informational clippy job fails on two pre-existing literals
(also failing on main, surfaced on this PR's merge): the trim
tangency test's angle list used 3.14 where pi was meant — use the
constant; the point-cloud light-direction fixture in render.rs uses
a hand-tuned 0.318 component that only looks like 1/pi — keep the
tuned value and allow the lint on that statement.
2026-10-11 21:30:54 +02:00
Karim Jerbi 9cf9ff2c5c style: apply cargo fmt across workspace 2026-10-11 03:14:29 +01:00
Karim Jerbi 7d008c4914 bench(navigation): add viewport navigation and spatial candidate benchmarks
- Add Section 19 benchmarks covering camera pan viewport uniform updates,
  spatial hover candidate search, and viewport data assembly scaling
- Add navigation telemetry probes in src/perf.rs (active only when
  PERF=1) to track frame interval, batched input moves, and layout-draw gap
2026-10-08 10:24:06 +01:00
Karim Jerbi 7aafd9c710 refactor(selection): split 31-field SelectionState into five sub-structs
SelectionState now holds view, gesture, input, menu and orbit sub-structs; all 450 access sites updated mechanically with no behavior change. Deletes three write-only dead fields (box_last, box_last_crossing, poly_last_crossing). Hand-rolled clears in commands and dim_viewport route through the new clear methods; bench fixture updated. Full suite: 1976 passed / 0 failed / 25 ignored.
2026-10-06 20:22:04 +01:00
PanosandClaude Opus 5 9fe2e4fa58 fix(view): hold ZOOMFACTOR to its own range, finish the exponential zoom
The exponential camera step is the right call. `0.9 ^ delta` is strictly
positive for every real delta, in and out cancel exactly where the
subtraction lost a percent of the view on every round trip, and the file's
own header has promised an "exponential feel" since it was written. Three
surfaces were left behind it, and the wider range exposed a fourth thing.

The range of the system variable. 3 to 100 is the range ZOOMFACTOR has, and
the User Preferences page was built against it; `SETVAR ZOOMFACTOR` accepts
that and nothing else again. A faster wheel is the Options window's business
instead: the slider covers the variable's own range, the field beside it
reaches 500, and a read-out says what a notch actually does ("10.0% a
notch") so the number being set is readable rather than implied. A value
above 100 therefore lives only in this application's own configuration file,
where it is nobody else's to read, and a bare `SETVAR ZOOMFACTOR` still
reports it. The bounds and the clamp live in one place now
(`settings::clamp_zoom_factor`): four literal copies of `3, 100` are what
let a surface be missed in the first place.

A floating viewport in MSPACE. It zooms its own view through
`zoom_active_viewport`, which kept subtracting: the factor reached zero at
6.67 steps, so from ZOOMFACTOR 360 up every notch inside a viewport bottomed
out on the clamp — a tenfold jump a notch, and the top third of the slider
doing nothing at all. Exponential there too, from a base of 0.85 so the
default notch still moves a viewport the 15% it always has. Each surface
keeps its own long-standing feel rather than inheriting the other's.

The trackpad pinch. `pinch_zoom_steps` was calibrated against the
subtraction (`1 - step / 10 = 1 / (1 + m)`) so that a pinch of m leaves the
view exactly 1 + m times closer — the property docs/trackpad.md documents
and the reason the gesture tracks the fingers at all. Inverting the zoom law
by hand is what let the two drift apart, so the step is asked of the camera
now (`Camera::zoom_steps_for_ratio`), which cannot drift by construction.
`pinch_is_one_to_one` recomputed the old formula rather than the camera's,
so it kept passing through all of this; it zooms a real camera and measures
the distance it lands at instead.

Also: `zoom` had a floor and no ceiling, and zooming out multiplies. At
ZOOMFACTOR 500 a notch out is x2.4, so about ninety notches — a couple of
seconds of scrolling — ran the distance past what an f32 far plane
(`distance * 1000`) can hold, and the viewport went black on an infinite
projection. A degenerate pinch magnification of -1 or beyond reached the
same place through the other side. Both are bounded now.

Also: two doc comments quoted the linear law (`zoom_view_at_cursor` and the
one on `pinch_zoom_steps`), and docs/trackpad.md stated the pinch identity
and claimed the tests covered it against the camera. All updated, with what
a notch does and where each surface's range comes from written down.

Tests, each confirmed against the behaviour it describes: the zoom law at
every factor the application can hold (positive, monotonic in the setting,
in and out cancelling), the ceiling under a long scroll out in both
projections, the published inverse and what it does with a nonsense ratio,
the viewport factor and a real viewport zoomed three notches at the top of
the range, the SETVAR range including a value the Options field set, the
Options field (above 100, above the ceiling, cleared, not a number), its
agreement with the slider, Close putting both the value and the field back,
and the read-out's percentages. 1882 lib tests pass.

The two new Options strings are not in the locale catalog yet, so they
render in English until it is regenerated.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-02 10:51:26 +03:00
F4brizi e131843826 feat(viewport): expand ZOOMFACTOR range to 500 2026-10-01 18:49:23 -03:00
Hakan SevenandClaude Opus 5.5 3dcebf0dc6 Merge pull request #1553 from ramox81/codex/pointcloud-attach
Point clouds: drawn from their scans, attached, cropped and stylized.

The fork's codec patch is resolved to the upstream pins: the point cloud
records landed in opencadcodec 5ef9376, carried through opencadkernel
68eeda1 and opencadgraph e204966.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 01:49:25 +03:00
Hakan SevenandClaude Opus 5.5 2a15a370f6 fix(dimension): an edited dimension keeps its layer's linetype
A dimension loaded from a file draws from its baked *D block, whose
lines are ByBlock and so take the dimension's linetype (ByLayer: the
layer's). Any edit - text colour, MATCHPROP, a grip - drops that block
and the dimension draws live, where a style with no DIMLTYPE/DIMLTEX1/2
drew solid: an edited dimension on a dashed layer turned solid (#898).
None or ByBlock now takes the dimension's own linetype, ByLayer its
layer's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 22:39:33 +03:00
ramox81andClaude Opus 5.5 15c0da8b79 Point clouds: measured lighting, highlighted extents box, gizmo shaft grab
- Lighting follows the view: the default source is a key light from the
  upper left and a fill light from the lower right over an ambient term,
  the other source one light from above; POINTCLOUDLIGHTING 1 lights the
  front faces, 2 both.
- A selected or hovered point cloud shows its extents box.
- A move-gizmo arrow can be grabbed along its shaft; the grab offset is
  taken on the press, so click-and-place and typed distances start from
  the insertion point.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 17:21:33 +03:00
ramox81andClaude Opus 5.5 b69ae291bb Point clouds: lighting, 2D visual style, real-time density
Points carry their world normal: POINTCLOUDLIGHTING lights them in the
shaded visual styles (1 lit from both sides, 2 with back faces dark),
from the view (POINTCLOUDLIGHTSOURCE 0) or the default lights (1).
The 2D wireframe style draws no cloud points: a found cloud shows its
extents and "Point clouds are not displayed in 2D visual style.",
or nothing with POINTCLOUD2DVSDISPLAY 1. While the view moves only
POINTCLOUDRTDENSITY per cent of the points are drawn; the points are
stored in an order where every leading part samples the whole cloud.
The colour map dialog lists its schemes by name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 14:54:03 +03:00
ramox81 3afd547e28 Merge remote-tracking branch 'origin/main' into codex/pointcloud-attach
# Conflicts:
#	Cargo.lock
2026-09-30 09:15:26 +03:00
Hakan SevenandClaude Opus 5.5 69a2e7c73a fix(ellipse): #1532 follow-up — one transform, in the codec
The PR's principal-axis math now lives in the codec's transform_ellipse
(8464e0a), which explode, INSERT and every other codec path share.
transform_ellipse_geometry delegates to it for the block-cache tangent,
and the entity goes back through apply_standard_entity_transform, with
MIRROR delegated to the codec's apply_mirror: the old closure reflected
only the centre and major axis, leaving mirrored arcs on the wrong side,
and the PR's version turned the normal to -Z. The mirror test now checks
the kept +Z normal and the negated parameters.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 23:04:23 +03:00
Hakan Seven df8ea089d7 Merge pull request #1532 from yztink/fix/ellipse-block-transform
fix(ellipse): accurately transform ellipse and elliptical arcs in blocks and standard transforms
2026-09-29 22:58:23 +03:00
ramox81andClaude Opus 5.5 e9c4e501db Point clouds: drawn from their scans, placed, cropped and attached as the reference does
Scans (.rcs) and projects (.rcp) are read and their points drawn as
screen-sized dots, coloured by the cloud's stylization. The cloud's
placement carries its scale and rotation; its extents and crops stay in
the scan's coordinates. A missing scan shows the extents with the saved
path. -POINTCLOUDATTACH / POINTCLOUDATTACH, POINTCLOUDCROP and the
POINTCLOUD* settings follow the reference's prompts and messages, the
Properties list is the reference's, and a locked cloud is left alone by
edits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 17:08:46 +03:00
Hakan SevenandClaude Opus 5.5 60f35e2b1d fix(color): only colour 7 swaps with the background; true colours draw as authored
Every near-white colour turned black on a light background (and near-black
white on a dark one), including true colours. A 255,255,255 solid used as a
white mask became a black block. Now only colour 7 swaps: true colours and
ACI 255 resolve a hair off pure white/black and keep their value on screen
and in plots.

Discussion #1500

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 06:16:18 +03:00
yztink a35fc949f3 fix(ellipse): accurately transform ellipse and elliptical arcs in blocks and standard transforms
- Add transform_ellipse_geometry helper in scene::view::transform to compute exact principal semi-axes, normal, ratio, and shifted arc parameters under any affine transform (negative scales, reflections, negative rotations, non-uniform scaling, and translations).
- Use transform_ellipse_geometry in block_cache::transform_tangent for TangentGeom::PlanarEllipse so GPU analytical instances and curve endpoints match true WCS positions.
- Update entities::ellipse::apply_transform to use transform_ellipse_geometry for complete scaling, rotation, mirroring, and affine transformations.
- Clean up tangent parameters in to_render for Ellipse so start_param and end_param are consistently unwrapped.
- Add comprehensive unit tests in transform.rs, block_cache.rs, and ellipse.rs verifying negative rotations, negative scales, and distant origins.
2026-09-28 16:09:41 -07:00
Hakan SevenandClaude Opus 5.5 fdc01e13c8 fix(render): show Inventor drawing views, each in its own viewport (#1285)
Inventor DWG exports place every sheet block with AcIdBlockReference,
which the codec skipped, so these drawings opened blank. opencadcodec
7c20aa7 decodes it as an INSERT and keeps the viewport each model-space
view reference is bound to.

Those view blocks overlap in model space, so a content viewport now also
hides model-space references bound to a different viewport. The handles
join the viewport's frozen-layer set (entity and layer handles never
collide), which the wire and render-graph visibility checks and every
per-viewport cache key already use.

Pins opencadcodec 7c20aa7, opencadkernel d71a828, opencadgraph 1543a5a.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 19:48:36 +03:00
Hakan SevenandClaude Opus 5.5 f537aff75c feat: node graph overlay; move to opencadcodec/opencadkernel/opencadgraph
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.

A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 07:34:23 +03:00
Karim Jerbi 3efd62b6ef perf(scene): optimize render environment scan and avoid insert clone in style spec
Eliminate full Insert entity cloning in block style evaluation, unify
document object environment scans, and guard viewport override queries.

1. Analysis & Architectural Bottleneck:
   - In render_graph::InsertStyleSpec::new, an entire Insert struct was cloned
     into an EntityType::Insert enum solely to pass to style functions that only
     read EntityCommon fields (layer, color, transparency, linetype, lineweight).
   - In view::render::resolve_document_render_environment, when object caches were
     not prepared, the document.objects map was traversed linearly twice: once
     for RenderEnvironment and once for RenderSettings.
   - Viewport override lookups were being performed even when viewport was None
     (the standard model space condition).

2. Optimizations Implemented:
   - Added `render_style_for_common_viewport`, `linetype_name_for_common_viewport`,
     and `has_resolved_book_color_common` taking `&EntityCommon` directly.
   - Constructed `InsertStyleSpec::new` without cloning the Insert entity.
   - Fused the two linear scans of `self.document.objects` in
     `resolve_document_render_environment` into a single pass tracking best candidates.
   - Guarded viewport override lookups with `if viewport.is_some()` in
     `layer_render_style_viewport` and `render_style_for_common_viewport`.

3. Correctness & Invariance:
   - The same fields and fallback values are resolved in identical order.
   - Tested across all 325 scene tests, block inheritance tests, and viewport
     override unit tests with 0 failures.

4. Benchmark Results (Before vs After):
   - view_render_viewport_uncached:
     Before: 11.00 µs (90.9k FPS)
     After:   5.00 µs (200.0k FPS)
     Improvement: -54.5% latency (2.2x faster uncached rendering)
   - wide_and_tapered_arc_tessellation:
     Before: 6.36 ms (235.8k entities/s)
     After:  5.41 ms (277.4k entities/s)
     Improvement: -14.9% latency (+17.6% throughput)
   - analytical_curved_tessellation:
     Before: 7.79 ms (359.5k entities/s)
     After:  7.01 ms (399.6k entities/s)
     Improvement: -10.1% latency (+11.1% throughput)
   - spatial_interaction_index_build:
     Before: 0.77 ms (2.47M wires/s)
     After:  0.73 ms (2.66M wires/s)
     Improvement: -5.1% latency (+7.7% throughput)
   - ui_grip_budget_build:
     Before: 424.50 µs
     After:  380.25 µs
     Improvement: -10.4% latency
   - ui_ribbon_view_construction:
     Before: 455.50 µs
     After:  382.30 µs
     Improvement: -16.1% latency
2026-09-24 09:05:08 +01:00
Hakan Seven 5719fcc86f Merge pull request #1430 from yztink:fix/viewcube-visibility 2026-09-22 12:28:01 +03:00
Karim Jerbi 874dd9debf perf(scene): optimize view render environment resolution and wire splitting
Eliminate major performance bottlenecks in the per-frame view render path:
- Memoize document render environment resolution using (geometry_epoch, len) keying, replacing per-viewport O(N) linear scans over document.objects.
- Index render_environments and render_settings handles in ObjectDataCache during the single build_cache pass.
- Cache resolved background image lookups in memory to eliminate repeated path.is_file() filesystem syscalls per frame.
- Add has_face3d fast-path to bypass wire splitting when no Face3D entities exist, and use FxHashSet<u64> handle indexing when present.
- Invalidate all cached structures across bump_geometry, bump_geometry_no_blocks, and bump_entities.
- Add comprehensive correctness unit tests and before/after benchmarks.

Performance impact (10k objects):
- P95 latency reduced from 341.00 µs to 173.00 µs (-49.3%)
- Throughput increased from 4,700 FPS to 6,000 FPS (+27.7%)
- Direct render environment evaluation: ~85 µs down to <50 ns (>1,700x)
2026-09-22 09:27:47 +01:00
yztink dad9c725c2 fix(viewcube): restore ViewCube visibility across display scales and window bounds 2026-09-21 23:39:48 -07:00
Karim Jerbi 4fdef48502 fix(viewcube): deterministic horizon for obliques, generic opposite
Top Front Right/Left kept Y (north) as up when starting from Top,
tilting the base because orbit is turntable (never banks). Make
snap_to_direction deterministic: world +Z horizon except near +-Z
where north +Y is used, sign-preserved only for intentionally
inverted views. Expand opposite() from faces-only to all 26 regions
so already-there flip works for edges/corners.
2026-09-19 09:37:56 +01:00
huanld 36d5c4db79 fix(render): compose PLOTSTYLE wire-fill depth like the wire pipeline
When a display plot style maps an ACI to a fill pattern, wire fill
triangles are re-emitted as PLOTSTYLE hatches. Those took
depth_override raw — a per-block child label for block wires and 0.0
for top-level ones — while everything else in the hatch/wipeout pass
sorts at the composed depths[insert] + label * half. The fill could
land outside its host's band and get buried under its own block's
masks (same shape as the block-text bug).

Both the display and PDF paths now travel through wire_draw_depth /
the PlotWire's composed depth, and the display cache key rides the
depth generation so a DRAWORDER edit re-ranks the fills without
needing a wire rebuild.
2026-09-18 08:38:35 +07:00
Hakan Seven af847ad92e Merge pull request #1299: Add Windows SpaceMouse navigation 2026-09-16 09:41:25 +03:00
Hakan Seven ffdb79a226 Merge pull request #1297: Fix perspective draw-order clipping 2026-09-16 09:35:59 +03:00
Hakan Seven 08bb17b0b8 Fix constraint editing and glyph feedback 2026-09-15 21:55:40 +03:00
Christopher Overbeck 6a63726eff Add Windows SpaceMouse navigation through 3DxWare 2026-09-15 13:20:19 -05:00
Christopher Overbeck e52e565b1f fix(renderer): keep draw-order offsets within clipping planes 2026-09-15 12:36:05 -05:00
Hakan Seven acde01cfc3 Merge pull request #1289 from ramox81/fix/coincident-standard-command
# Conflicts:
#	src/command.rs
#	src/scene/mod.rs
#	src/scene/parametric_solve.rs
2026-09-15 15:08:35 +03:00
ramox81 ba2b371269 Complete Coincident command behavior 2026-09-15 14:00:26 +03:00
Hakan Seven c9eeb6f66e Merge pull request #1284 from ramox81/codex/autoconstrain-parity
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/app/view/mod.rs
#	src/ui/overlay.rs
2026-09-15 12:38:41 +03:00
ramox81 66be530655 Complete automatic constraint behavior 2026-09-15 11:47:28 +03:00
Christopher Overbeck 37cfa06b02 Preserve fractional viewport placement when compositing paper space
Keep fractional raster placement and matching integer texture coverage
through compositing. Apply parent clipping without changing the scale.

Fixes #1267
2026-09-14 17:07:57 -05:00
Hakan Seven 78ab1c8013 Keep grip edits on the drawing plane and preserve constraint undo 2026-09-13 22:33:17 +03:00
PanosandClaude Opus 5 02b0f4c7d9 Tell the user when the scene is drawn without a GPU
A day of pan/zoom that had suddenly become unusable on a 1.2M-entity
drawing turned out to be no regression at all: an unattended NVIDIA
userspace update had been applied without a reboot, the Vulkan ICD could
no longer talk to the still-loaded kernel module, and wgpu silently fell
through to Mesa's llvmpipe. Nothing in the app said so. iced picks the
adapter and names it only under RUST_LOG, so the only evidence was 32
`llvmpipe-N` threads in /proc. The same silence covers WARP on Windows,
SwiftShader in browsers, and the case where wgpu finds no adapter at all
and iced quietly draws the chrome with tiny-skia while the shader widget
is dropped, leaving a blank viewport. This commit makes all of that
visible, on every platform, without new dependencies.

Detection (src/scene/pipeline/mod.rs)
-------------------------------------
`GpuStatus` is derived from two signals:

* `record_gpu_adapter`, run from `MultiPipeline::new` (once per device,
  next to the existing uncaptured-error handler), reads
  `Device::adapter_info()`, prints one `[gpu] adapter: <name> (<backend>,
  <device type>, driver …)` line — stderr natively, `console.warn` on the
  web, which has no stderr — and keeps a copy.
* `note_scene_draw`, run from `ViewportPane::draw`, counts how often the
  viewport asked for a primitive. Every renderer makes that call; only
  wgpu goes on to `prepare` and create a device. Three draws with no
  device ever recorded therefore mean the primitive is being dropped.

The verdict is one of `Unknown`, `Hardware`, `Software(adapter)` or
`NoRenderer`. `Software` is `DeviceType::Cpu` — what Vulkan reports for
llvmpipe, DX12 for WARP and GL for a renderer string that gives it away —
plus a name match on the fallbacks browsers expose (SwiftShader, llvmpipe,
lavapipe, "Microsoft Basic Render"), because the WebGPU backend reports
every adapter as `Other` (gfx-rs/wgpu#8819). Metal has no software device,
so on macOS only `NoRenderer` can occur (a VM without a GPU); it is caught
by the draw count like everywhere else.

`gpu_status_if_changed` costs one atomic load per app message: a
generation counter moves only when a device is recorded or the draw count
crosses its threshold, so the app re-derives the verdict a handful of
times per session.

Notification (src/app/startup.rs, view/modal.rs, ui/statusbar)
-------------------------------------------------------------
`refresh_gpu_status` runs after every message. On a degraded verdict it
speaks through three channels, because many users never read the command
line:

* A popup, `ModalKind::GpuWarning`, queued once per verdict through the
  startup-modal queue so it waits its turn behind the file open. It names
  the adapter, states the consequence (slow navigation, unstable or
  missing rendering) and gives the remedy that usually applies on the
  running platform: a driver update not yet followed by a reboot on
  Linux, a missing driver or a Remote Desktop / VM session on Windows, a
  VM without a Metal device on macOS, hardware acceleration switched off
  in the browser. "Don't show again for this device" stores
  `GpuStatus::identity()` in `settings.gpu_warning_silenced`; keying it by
  verdict means silencing "software rendering on llvmpipe" does not
  silence a later, different failure.
* A status-bar pill in the theme's warning colours, `⚠ Software rendering`
  or `⚠ No GPU renderer`. It is not a `StatusPill`, so it cannot be hidden,
  and it stays for the session; clicking it reopens the popup, silenced or
  not.
* A command-line entry through the new `CommandLine::push_warning`, which
  is styled like an error but deliberately does not touch `last_error` /
  `error_revision`: automation reads those to decide whether the command it
  just ran failed, and a graphics warning is not a failed command.

`subscription()` requests frames only while the verdict is `Unknown` on a
document tab (the Start page draws no viewport), so the probe costs one
frame on a working GPU, three on none, then stops.

Verification
------------
* Live on the machine that had the incident, with the Vulkan loader
  restricted to Mesa (`VK_DRIVER_FILES=…/lvp_icd.json WGPU_BACKEND=vulkan`):
  popup → OK → pill → click reopens; "Don't show again" → restart → pill
  only, no popup.
* `gpu_tests::the_adapter_is_reported_once_per_device` (ignored, needs an
  adapter) asserts the classification on a real device; it was run on both
  llvmpipe (`Cpu`) and an NVIDIA GT 1030 (`DiscreteGpu`).
* Unit tests cover the name heuristic, the verdict function, the identity
  rule, the warning text with an adapter name that itself contains
  parentheses and commas, the settings round trip and the command-line
  semantics. New strings are in all 21 locales as the `gpu` Fluent group.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-12 16:19:41 +03:00
ramox81 6272722baa Carry hatch origin through creation and pattern editing 2026-09-11 13:40:40 +03:00
Karim Jerbi f0cc4ffba8 fix(camera): prevent near-plane clipping during 3D orbit on wide strokes and flat drawings 2026-09-09 13:02:02 +01:00
Hakan Seven 4972bbed70 Fix issues found while integrating performance PRs 2026-09-08 17:33:02 +03:00
Hakan Seven 2b510c1a1e Merge pull request #1076 from mechpanos/pr/skip-partition-on-patch 2026-09-08 17:17:59 +03:00
PanosandClaude Opus 5 eef007d080 perf: report a skipped partition without printing zero counts
The skip branch printed `circles=0 ellipses=0`, which reads as "there are
no circles" when it means "not recomputed this frame" — and those counts
were the evidence that confirmed the skip works, so the wording nearly
cost a misreading of it.

The skip now prints its own line with what it retained.

Measured effect of the skip itself, on the reproducer: a drawn point goes
from 98 ms to 17-39 ms, `prepare` from 89 ms to 11-23 ms. Frames that draw
a circle still re-partition, and the instance counts show it — 40868,
40869 … 40874, one per circle, with the line frames skipped in between.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:39 +03:00
PanosandClaude Opus 5 03c64e3d38 perf: skip the wire partition when a patch cannot change its output
`arena-post partition=55.2ms blocks=5.9ms curves=2.5ms` — the partition is
almost the whole of the 73 ms an arena patch spends after patching, and it
walks all 236 956 resident wires to serve a one-entity change.

Its three consumers here — the instanced, circle and ellipse uploads —
depend only on wires that are block references or analytical curves. A
patch that touches none of those cannot change any of them, so the walk is
skipped and the uploads already on the slot are kept.

Two things make that sound, and both are checked rather than assumed:

**Depths hold still.** The instances bake a draw depth.
`inserted_draw_depth_label` allocates into a reserved gap instead of
renumbering, so adding an entity leaves every other label alone — the
existing doc comment says so and there is now a test. A rebuild from
scratch *can* move them, so `draw_depth_generation` bumps only there, and
a slot reuses its uploads only while that generation is unchanged.

**Contributors are remembered.** `partition_wires` records which handles
fed those three outputs, and the skip requires that no changed handle is
among them and that no changed handle's new wires would join them. That
catches an entity entering the set and an entity leaving it. The
membership test is shared with the walk it decides to skip, so the two
cannot drift, and it is deliberately conservative: a wire that looks
analytical but fails extraction forces a needless re-partition, never a
stale upload.

`arena-post` gains `skipped=` so a run says how often this actually fires
rather than leaving it to be inferred from the timing.

Not verified on a device yet — the headless harness never reaches
`prepare`, so this needs a GUI run on the reproducer to confirm both the
saving and that nothing goes missing on screen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:38 +03:00
PanosandClaude Opus 5 feb3ca4d28 perf: split the arena patch's post-work into its three steps
With the selection-filter rebuild gone, a drawn point costs 98 ms and
`prepare` is 93 % of it:

    wire  73.0ms  arena-patch  wires=236961 gpu_instances=405551
    wire-cull      5.1ms
    face3d-upload  3.9ms
    text-upload    3.0ms
    resident-patch 1.0ms  changes=1
    draw-depth-patch 0.0ms

`arena-base ok=true patch=Some(16) changes=1` — the arena patches
incrementally, one entity, and still takes 73 ms. The patch itself is
careful; the three steps that follow it are not, and all three are
O(resident set) rather than O(changes):

    partition_wires(&vp_wires, …)          236 961 wires, every frame
    upload_block_wires(…instanced…)         10 719 placements rebuilt
    upload_circles/ellipses_from_instances

`arena-post partition= blocks= curves=` says which dominates. Guessing
between three plausible candidates is what the earlier rounds of this work
paid for repeatedly.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:38 +03:00
PanosandClaude Opus 5 d267083a29 perf: say which message caused an edit's latency
The first measurement showed four consecutive edits at ~192 ms each with
a handler cost of 0.55 ms. That is below the `update` line's 5 ms
threshold, so the log reports the latency but not what asked for it —
and whether a live command preview is bumping the geometry epoch on every
mouse move is exactly the question the numbers raise.

`nav-prepare` now carries the message label alongside the op.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:21 +03:00
Karim Jerbi dd061a55ed perf(render): single-pass viewport wire classification and partitioning
- Add fast classify_wire to bypass circle/ellipse extraction for the 95%+ of wires that are straight/polylines.
- Implement single-pass partition_wires extracting regular wires, mesh edges, block wires, circles, and ellipses in one traversal.
- Upload circle and ellipse instances directly from pre-extracted buffers in PersistentWireArena and build_wire_buffers without redundant iteration.
2026-09-07 13:55:16 +01:00
Karim Jerbi 0425f979f9 feat(render): GPU analytical rendering for circles, arcs, and ellipses
- Render planar circles, circular arcs, ellipses, and elliptical arcs analytically via GPU-instanced screen-space quads and WGSL shaders.
- Implement exact arc sweep clamping with round end caps matching the CAD wire shader.
- Implement robust Newton-Raphson closest-point solver for ellipses with isotropic screen-space derivative anti-aliasing.
- Integrate into wire pipeline and PersistentWireArena upload paths.
- Remove unused mesh_volume_centroid function in doc_api.
2026-09-07 13:50:12 +01:00
Hakan Seven ac8a545669 Fix layout cache correctness and GPU resource recovery 2026-09-05 19:56:49 +03:00
Hakan Seven 1a4b389b0e Merge pull request #1014: perf: let one tessellation serve every background 2026-09-05 19:28:34 +03:00
PanosandClaude Opus 5 391a74a615 perf: let one tessellation serve every background
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`,
2034 ms — to arrive at geometry identical to what was already in the memo.
The only thing that differed was colour.

`local_wires_for` already knows this: it stores the raw colour and drops
its background argument with `let _ = bg_color;`, noting that
`Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer
has to rebuild on BACKGROUND / layout-switch". The tessellation memo's
guard had not been told, and still mixed the background in, so every
entry was discarded on a switch.

`finalize` now records what its colour resolution consumed — the raw
colour, the raw glyph colours, and the three flags it branches on — as
`WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another
background. The guard drops the background, so a memo entry serves any
layout.

The resolution recomputes from the recorded raw colour rather than from
the resolved one, which makes it idempotent and therefore safe over a set
that mixes memo hits with freshly tessellated misses. Re-adapting the
resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour
to pure black on a light background, and pure black comes back pure
*white* on a dark one, losing the tint. A test pins that asymmetry so the
raw colour is not optimised away later.

The contract is tested against the real `finalize` rather than a copy of
its logic: for every background pair, building under one and resolving for
the other lands exactly where building under the other would have.

`bg_adapt` is `None` for wires built anywhere else, and the resolution
skips them, so nothing outside the block cache changes colour. It is also
`None` when the resolution would be a no-op — a preserved colour with no
canvas fill looks the same under every background — which keeps the common
case at no cost.

Ordering is unchanged: the pass runs inside the builder, before
`append_scene_markers` and `apply_refedit_fade`, both of which have their
own separate background dependency.

`wires-build` gains `colors=` so the pass has a number of its own.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 18:53:16 +03:00
PanosandClaude Opus 5 49318a1c55 perf: allocate a shadow map only for viewports that cast shadows
`gpu-live` made this the obvious next one. Once idle slots started giving
their memory back, the shadow maps were the largest thing that never did:
128 MiB across eight slots, 37 % of the 341 MiB floor, and none of it
sampled — `shadow_enabled` defaults to false and most visual styles never
turn it on.

Every slot allocated `SHADOW_MAP_SIZE` squared at `Depth32Float` in
`Pipeline::new` and held it for the life of the slot, because the frame
bind group needs *something* at its shadow binding and the field was not
optional.

It is now `Option<(Texture, TextureView)>` alongside a 1x1 placeholder
that satisfies the binding while shadows are off — nothing samples it
then, so one texel is the whole requirement. `sync_shadow_target`
allocates on the transition into shadows and drops on the way out, and
only on the transition, so a viewport that keeps its setting does not
rebuild a bind group every frame.

The frame bind group had two inline builders — one in `Pipeline::new`, one
in `upload_background_images` — and this adds a third caller, so they are
now one `rebuild_frame_bind_group`. A shadow transition cannot forget the
background image the slot already had.

The shadow pass now tests for the target rather than the flag: wanting
shadows and having somewhere to put them are different questions, and
without the second one a failed 16 MiB allocation would have rendered the
pass into the 1x1 placeholder. Releasing a cold slot hands the target back
too, and `gpu-live` reports what is actually held rather than a constant.

Covered by a GPU test that toggles the setting off/on/off/on against a
real adapter with a validation scope armed, then releases the slot — the
transition is the risky path and the other GPU tests never take it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 18:53:09 +03:00