Commit Graph
1121 Commits
Author SHA1 Message Date
ramox81 7913f8e2da Merge remote-tracking branch 'origin/main' into codex/shell-parity
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/ocs_doc_api/Cargo.toml
#	src/modules/model/mod.rs
2026-09-08 19:49:35 +03:00
ramox81 be06a756da Implement solid shell workflow and properties 2026-09-08 19:37:35 +03:00
Hakan Seven fd3ddc1608 Finish Ramox edge and slice integration 2026-09-08 19:02:08 +03:00
Hakan Seven 39492860d3 Merge pull request #1077 from ramox81/codex/filletedge-split
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/app/model_ops.rs
#	src/scene/model/solid_history.rs
2026-09-08 18:33:20 +03:00
Hakan Seven 7ec07f3222 Merge pull request #1070 from ramox81/codex/filletedge-parity
# Conflicts:
#	Cargo.lock
2026-09-08 18:30:34 +03:00
Karim Jerbi 08891816be fix(render): highlight untinted analytical curves and guard uniform bind group
- Fall back to WireModel::SELECTED when selected_tint is None (classic mode) so selected circles, arcs, and ellipses are not omitted from the highlight pass.

- Explicitly rebind uniform_bind_group before drawing analytical curves in selection_xray pass.

- Skip invisible wires (display_visible == false) during selection and hover wire extraction.
2026-09-08 16:21:28 +01:00
Karim Jerbi 058cd02042 fix(pick): require all sub-geometry inside window selection for multi-wire and mesh entities 2026-09-08 15:49:54 +01:00
ramox81 f16423ef8e Keep primitive properties editable after fillet 2026-09-08 17:35:51 +03: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
Hakan Seven 1d9244177e Merge pull request #1075 from mechpanos/pr/open-and-selection-cost 2026-09-08 17:17:53 +03:00
Hakan Seven d710868d89 Merge pull request #1074 from mechpanos/pr/selection-filter-perframe 2026-09-08 17:17:45 +03:00
Hakan Seven 2ba5415479 Merge pull request #1073 from mechpanos/pr/edit-latency-metric 2026-09-08 17:17:37 +03:00
ramox81 261ab66afd Restore primitive properties after edge operations 2026-09-08 17:11:23 +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 5eb762e88d fix: forget the partition record with the keys that describe it
`forget_cached_keys` is the one place that means "this slot holds nothing
you can trust", and it left the partition record behind. A reused slot
rebuilds regardless — its content id changed, so the arena takes the build
path and the record is overwritten — but that made the safety an accident
of another code path rather than something this function guarantees.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:39 +03:00
PanosandClaude Opus 5 d46c00281f fix: decide partition membership by extraction, not by shape
`skipped=false` on every frame of a real session — the skip added in the
previous commit never fired once.

The membership test used the same cheap structural filter that
`partition_wires` uses before attempting extraction: a tangent geom, no
fill, pick or text triangles. That filter is a pre-check, not a decision.
An ordinary drawn line carries a tangent geom for snapping and no
triangles, so it passed, and every drawn line was counted as feeding the
analytical uploads. Nothing was ever skippable.

It now decides the way the walk decides, by attempting the extraction.
`draw_depth` only rides along in the extracted instance, so the value
passed for the test cannot change the answer, and it runs only for the
handful of wires a patch changed.

Drawing a circle or an ellipse still forces the walk, correctly — those
are contributors, and the session that exposed this contained both. What
changes is that the lines drawn afterwards no longer do.

Tested on real tessellated geometry: a line must not force the walk, a
circle must.

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 b68989768d perf: look the draw depth up only where it is used
`arena-post partition=55.2ms blocks=5.9ms curves=2.5ms` — the partition is
the whole of the 73 ms an arena patch spends after patching, and it runs
on every drawn point over the entire resident set.

Inside it, `wire_draw_depth` is a hash lookup whose result feeds only the
analytical circle and ellipse extraction, and it was computed before the
test that decides whether either will run. So every patch paid 236 956
lookups to serve the ~56 000 wires that are circles or ellipses.

Moving it inside that branch is behaviour-preserving: the value was
unused on every path that skipped it.

This does not make the partition incremental — it still walks the resident
set for a one-entity change — but that walk is memory-bound and a
structural fix needs the cached partition to survive a patch, which the
borrows in `PartitionedWires` make its own piece of work.

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 6f01d4215b perf: fold a lone addition into the block-member index
`vis_index=19.4` of a 55 ms selection, on every build, because the index
is keyed on `geometry_epoch` and the epoch moves on every edit. The walk
visits every entity in the drawing to add one handle to one list.

A lone addition is folded in instead. **Only** a lone one: entities are
appended to the document, so a single new handle belongs at the end of its
block's list, whereas `replay_since` returns several changes in no
particular order and each list must stay in document order because that is
draw order. A removal, a modification that could move an entity between
blocks, or more than one change all take the full walk.

Measured on a 586 k-entity drawing, headless, opening then drawing two
lines with a rebuild after each:

    before   visible=50.7  vis_index=19.7      visible=48.2  vis_index=17.6
    after    visible=34.1  vis_index=0.0       visible=34.4  vis_index=0.0

A third of the selection, and it holds for each subsequent edit.

Tested against the full walk, including that the appended handle lands
last — putting it anywhere else would reorder the drawing.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:21 +03:00
PanosandClaude Opus 5 5e5a810ee5 perf: stop rebuilding the selection filter list on every drawn point
`view-detail` on an edit frame:

    total=105.6ms  chrome=105.4   [every other section 0.0]

and the edit latency measurement puts ~102 ms of a 192 ms drawn point in
the gap between the handler returning and the scene primitive being built
— which is that same `chrome`.

The status bar asks for `entity_type_names_in_layout` on every frame to
populate the selection-filter menu, and the cache behind it was keyed on
`geometry_epoch`. Every drawn point bumps that epoch, so every drawn point
walked all 444 937 handles a model space owns to answer a question the
line did not change: adding a line to a drawing that already has lines
leaves the set of type names exactly as it was.

The cache now carries the set alongside the list, and folds edits in from
the delta journal — the same incremental shape `entity_index` already
uses. Additions can only grow the set, so they are applied directly and
the existing `Arc` is handed back untouched when nothing new appeared. A
removal can retire a type and cannot be resolved from a handle that is
already gone, so those still fall back to the full walk.

`entity_type_name` returns a literal for every arm but one — `Extended`
borrows its class name from the entity — so the set holds owned strings,
and the hot path tests `contains` before allocating.

Tested: a second line of an existing type must hand back the same `Arc`,
a new type must still appear, and the folded answer must equal what a
rebuild from scratch produces.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:21 +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
PanosandClaude Opus 5 41b794e9a7 perf: resolve each layer once per selection, not once per entity
`Table::get` normalises the name with `to_uppercase`, so every layer
lookup allocates a String and runs a Unicode pass.
`resident_entity_visible` did one per entity, and the selection calls it
once per candidate to look up a few hundred distinct layers.

The predicate splits so the layer can be passed in, and the selection
resolves each distinct name once into a small map keyed on the entity's
own layer string. Keys borrow from the document, so naming the lifetime on
`wires_for_block_culled` is what lets them outlive the closure call.

**Measured, and smaller than it looks.** On a 585 786-entity drawing with
37 826 candidates, A/B through `OCS_NO_LAYER_MEMO`:

    with memo   visible=55.1ms
    without     visible=57.1ms

3.5 %, not the order of magnitude the allocation count suggested. The
per-entity cost of that selection is dominated by traversing entities
scattered across the document arena, not by the work done at each one — a
resolve-only pass over the same handles measures 1.4 ms, and only because
it runs second, on a warm cache.

Kept because it is measured, cheap and correct, not because it is the
answer to the selection's cost. `OCS_NO_LAYER_MEMO` stays so the claim can
be rechecked on other drawings.

The keying is on the **raw** name while the table is case-insensitive, so
two spellings of one layer take two slots and both resolve to it — one
extra normalisation, never a different answer. Tested, along with the
predicate that reads it.

The early cheap checks moved into the layer-taking variant rather than
staying in the wrapper, so calling it directly cannot skip them.

`vis_index=` reports what `block_members` costs inside `visible`: 19.2 of
the 55 ms here, rebuilt by walking the document once per `geometry_epoch`
— which moves on every edit.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:21 +03:00
PanosandClaude Opus 5 d5859a01a5 perf: separate the two costs inside the visible-entity selection
The first drawn point costs 1068 ms and `wires-build` owns 710 of it:

    visible=402.5  build=148.4 [classify=18.4 hits=127.5]  sort=41.9
    memo_hit=186455 memo_miss=1  candidates=444938

The memo is doing its job. `visible` is not, and two things in that loop
could own the 402 ms: resolving each handle through the document's hash
index, or the predicate — which reaches `document.layers.get(&c.layer)`,
a **string-keyed** lookup, so a string hash per entity, 444 938 times.

They call for different fixes, so `visible_probe=` times a resolve-only
pass and the remainder is the predicate. The pass repeats the resolve
rather than reading the clock inside the loop, where the measurement would
cost more than the thing measured, and it runs under PERF only.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:21 +03:00
PanosandClaude Opus 5 6b03e4cf17 perf: hand the loader's draw-order map to the scene
The open sequence computes it twice:

    [perf] draw-depth 293.5ms entries=1166328    ← loader thread
    …
    [perf] draw-depth 302.0ms entries=1166328    ← main thread
    [perf] update    1427.3ms message=other

`DrawDepthCache` is keyed on `geometry_epoch`, the loader builds it on its
temporary scene, and `PreparedOpenGeometry` carried the wire set, the
tessellation memo and the block definitions across — but not this. So the
receiving scene walked all 1 166 328 entities again to rebuild a map that
already existed, inside the message that finishes opening the file.

It travels on exactly the terms the block definitions already do: the
document is the loader's own and has not been touched between the loader
finishing and the install, so its order labels describe the receiving
scene exactly. Only the epoch is restamped.

Tested by handing a computed map back into a scene through
`install_prepared_open_geometry` and asserting both that it is served
as-is and that it equals what that scene recomputes from scratch — the
second assertion is the one that matters, since serving a map that does
not describe the document would put every entity at the wrong draw order.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:21 +03:00
PanosandClaude Opus 5 b4ae4cad4e perf: measure an edit from the input to the frame that answers it
Drawing a point in a large drawing feels slow, and nothing currently
measures the thing the user actually waits for. `update` times the message
handler, `wires-build` and friends time the rebuild, but the span between
them — press to the frame that shows the new geometry — has no number.

Navigation already has exactly that plumbing: `record_nav_perf` stamps an
instant when the input arrives, `prepare` and `encode` pick it up, and
`nav-prepare` / `nav-render` report `input`, `build`, `prepare` and
`elapsed` from the original event.

`NavPerfOp::Edit` reuses it. `update` snapshots `geometry_epoch` before
dispatch and stamps the sample when it moved, so any message that changed
geometry — a drawn point, a grip commit, an undo — reports its full
latency rather than only the handler's share.

Diagnostic only: `record_nav_perf` sets a pending sample and nothing else,
so no LOD or settle behaviour changes.

Nothing is optimised here on purpose. Four call sites feed the snapper
along different branches, and picking one to instrument before knowing
which dominates is how the earlier rounds of this work were wasted.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 16:42:20 +03:00
Karim Jerbi 116c4836a1 fix(render): render selection and hover highlights for analytical circles and ellipses 2026-09-08 14:39:37 +01:00
ramox81 8822371e18 Complete CHAMFEREDGE workflow and history 2026-09-08 15:45:10 +03:00
Karim Jerbi a754fac6d4 Attach analytical arc to polyline Arc mode rubber-band preview 2026-09-08 13:16:49 +01:00
Karim Jerbi ec3b6b89f6 Expand GPU analytical curves to interactive previews and transformations 2026-09-08 13:16:49 +01:00
ramox81 8bb6703c70 Complete FILLETEDGE workflow and history 2026-09-08 12:36:49 +03:00
gianlucafiore b7c2ce2e13 fix(hatch): refresh associative hatch during geometry edits 2026-09-07 18:18:36 -03:00
Karim Jerbi d988a09987 feat(render): split mixed polyline bulge arcs into GPU analytical curves and tag benchmark 2026-09-07 17:22:01 +01:00
Hakan Seven e51c4d0932 Merge updated origin/main 2026-09-07 19:01:54 +03:00
Hakan Seven d5047b49d9 Merge PR #1053: complete INTERSECT behavior 2026-09-07 18:43:49 +03:00
Hakan Seven 7c2a4d63ff Merge PR #1051: complete SUBTRACT behavior 2026-09-07 18:36:04 +03:00
Hakan Seven 050fbb0a6b Merge PR #1045: complete UNION behavior 2026-09-07 18:36:04 +03:00
Karim Jerbi e603ef6104 test(bench): add end-to-end analytical rendering benchmark 2026-09-07 16:14:17 +01:00
Karim Jerbi 75ec08dc55 feat(render): GPU analytical curve extraction for multi-arc polylines and blocks 2026-09-07 14:59:50 +01:00
Karim Jerbi 822b0afce5 Convert polyline bulge segments to analytical TangentGeom::Arc
- Add bulge_arc_to_tangent helper to convert 2D BulgeArc into a 3D
  TangentGeom::Arc preserving center, plane axes, radius, and CCW sweep angles.
- Replace full TangentGeom::Circle emission with TangentGeom::Arc across
  LwPolyline and Polyline2D paths.
- Enables single-arc polylines to be extracted and rendered as analytical
  GPU arcs with circle.wgsl.
- Add unit test verifying single bulge polyline extracts as analytical GPU
  arc.
2026-09-07 14:48:56 +01:00
Karim Jerbi 73cd759ca4 Extend GPU analytical circle and arc rendering to block inserts
- Emit transformed analytical curves (circles, arcs, ellipses) in block
  expansion as standalone wires rather than merging them into batched line
  strips.
- Keep render_instance unset on analytical curves in block prototypes so
  they route directly to CircleGpu / EllipseGpu instead of BlockWireGpu.
- Support TangentGeom::Circle in extract_circle_instance and wire partitioning.
- Drop analytical tangents on wires clipped by xclip so clipped geometry
  falls back correctly to line segments.
- Add unit test verifying block circles and arcs extract as analytical GPU
  instances.
2026-09-07 14:43:38 +01:00
Karim Jerbi 690971c44e Make tessellation memo and resident wire set zoom-independent
With GPU analytical rendering handling circles, arcs, and ellipses,
tessellation geometry is now camera- and zoom-independent.

- Remove wpp from the tessellation memo guard hash to prevent clearing
  all cached entities when zooming across quantization bands.
- Remove quantized_wpp from resident_wire_key and resident_wires_for
  so all zoom levels reuse the cached resident set directly with zero
  CPU re-tessellation.
- Fix clippy slice_from_raw_parts lint in dwg-thumbnailer without suppression.
- Update zoom test to assert Arc pointer equality across camera zooms.
2026-09-07 14:32:55 +01:00
ramox81 1e118b2e90 Complete exact INTERSECT behavior 2026-09-07 16:32:01 +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
ramox81 eb83e551b8 Make Solid History choices editable 2026-09-07 15:05:45 +03:00
ramox81 04a9a1b0a0 Complete SUBTRACT behavior 2026-09-07 13:58:31 +03:00
ramox81 5857ffa2bd Align UNION solid properties palette 2026-09-07 12:02:59 +03:00
ramox81 a59ea47990 Complete UNION command behavior 2026-09-07 11:27:03 +03:00