Double-click / "Open With" never puts the file on argv — Finder always
delivers it as a kAEOpenDocuments Apple Event instead, which nothing in
this app listened for, so it just showed "cannot open files in this
format" even though the file association itself was registered
correctly.
The standard fix, implementing application:openURLs: on this app's own
NSApplicationDelegate, isn't available here: winit already installs
its own delegate to own window/lifecycle events, NSApplication has
exactly one delegate slot, and winit's class doesn't implement the
open-document method.
This bypasses the delegate protocol and registers directly with the
lower-level Apple Event Manager (AEInstallEventHandler) instead — the
same mechanism AppKit's delegate forwarding is itself built on, and
independent of who owns the delegate. Registration happens on the
iced Subscription's first poll rather than in main() before app::run()
— registering earlier gets silently overwritten by winit's own
NSApplication startup, confirmed by testing against a real `open -a`
rather than assumed from Apple's docs.
This handles a file opened while the app is already running. A cold
launch (app not running yet) needs a different mechanism, in the
following commit.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Add the typed document facade and plugin IPC adapter. Prepare bulk edits
before mutation, preserve undo and dirty state, keep profile elevations,
validate numeric inputs and session bindings, and generate Python constructors
with typed results.
Reuse existing entity transforms and profile conversion. Compute mass
properties and closed-surface bounds in cadkernel; pin its main revision
6a5e6fc2822f6af6b2611cd605b44437b35158f0. Keep plugin API metadata at version 6.
Validation: 646 native application tests, 597 kernel tests, 12 document API
tests, 91 plugin API tests, 3 Python tests, default/IPC feature builds and
wasm32 check passed.
Delete the thin re-export file; src/app/doc_api.rs now uses ocs_doc_api::convert
directly (use ocs_doc_api::convert + convert::* call sites). Remove the
mod doc_api_convert registration from src/app/mod.rs.
The conversion logic already lives in the crate (src/convert.rs) from the prior
move; the shim was redundant. 623 host lib + 38 doc_api tests green.
Move the 503-line host-independent entity-conversion module (acadrust EntityType
<-> DTO conversions, transform_entity_geometry, entity_to_profile_curves,
bulge_arc_segment, entity_bounds, entity_kind_name, points_aabb) from
src/app/doc_api_convert.rs into crates/ocs_doc_api/src/convert.rs (host feature).
The host file is now a thin re-export (pub use ocs_doc_api::convert::*) so all
existing crate::app::doc_api_convert::* paths keep working unchanged. Zero
coupling risk (the module only touches acadrust + crate DTOs).
623 host lib + 8 crate + 3 consistency green.
- layouts.json: append the 6 missing ops (CreateDimensionRadius/Diameter/Angular,
CreateAttributeDefinition, CreateTable, CreateDimensionAngular2Ln); regenerate
binding_schema.json + api_reference.md wire vocabulary (all 28 ops).
- Helix bounds: bound the evaluated spline control polygon (was a degenerate
single-start-point AABB).
- points_aabb: empty -> Geometry::Empty('entity has no points to bound'), not
UnknownId (the entity exists; don't misclassify as stale/deleted).
- mass_properties: accuracy-first fine tessellation + graceful degradation to the
approximate meshes.metrics value when the cold-tess budget is exhausted (large
distinct-solid batches no longer hard-fail).
- IMPLEMENTATION.md: Volume/Centroid rows + accuracy paragraph + test count (618)
consistent with the final design.
- 618 host lib + 8 crate + 3 consistency green.
- mass_properties: query path now uses fine mesh_body(0.1, 1e-4) for near-analytic
volume/centroid; the render-mesh metrics cache (coarse display LOD) no longer
shadows query accuracy. Memoized per (handle, epoch); cold-tess budget kept.
- IMPLEMENTATION.md: accuracy item done.
- Test accurate_volume_centroid_sphere_and_cube (sphere within 0.5% of 4/3*pi*r^3,
centroid at centre, cube exact) -> 617 host lib green.
Capture the rendered canvas during the clean frame instead of requesting synchronous GPU buffer readback. Preserve embedded previews and allow saving when canvas readback is denied. Add a browser save and reopen regression check.
Fixes#1021
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]>
Switching to a Layout stops the main thread long enough that the
compositor decides the application has stopped responding and offers to
kill it. The switch itself is not the problem — `layout-switch` reports
about 10 ms — it is the frame after it, which rebuilds the whole wire set.
That frame now has a cheap one in front of it. `layout_settling` is set
when the switch takes, and while it holds, the viewport is replaced by a
centered "Preparing layout…" on the canvas background. No shader widget
means no scene primitive, so `prepare` has nothing to do and the frame
costs nothing. A `window::frames` subscription clears the flag, which is
the guarantee this needs: that callback runs once a frame has actually
been presented, so the notice is on screen and the compositor has been
answered before the expensive frame starts.
The deferral is of the *frame*, not the switch. Two callers depend on the
switch having completed when it returns — one calls `fit_all` immediately
after, another compares `current_layout` — so deferring the switch itself
would have broken both.
This restarts the compositor's timer immediately before the slow frame and
replaces a stale window with something that says what is happening. It
does not make the slow frame fast: if the rebuild still exceeds the
compositor's timeout the dialog can return, and the durable fix is the
2 s `memo_miss` behind it, which is a separate change.
Only `en-US` is validated against at build time; Greek is filled in too
since it is in use here, and the rest fall back until translated.
Co-Authored-By: Claude Opus 5 <[email protected]>
`view-detail` located the first paper frame's cost precisely: 7826.9 of
7832.8 ms in `grid`. Once, on the first frame after a layout switch —
every other frame in a 2769-frame session reports it at 0.0.
That is the largest single cost in a layout switch, larger than the model
re-tessellation and the arena rebuild combined, and it is not geometry
work at all. `grid_overlay` itself only wraps a canvas; `grid_segments`
runs later, in `draw`. So the time is in `grid_views` or in the per-view
parameter build, and reading the two does not settle which: both reach
helpers that can fall back to a full document walk, and neither obviously
fires for this drawing.
`grid-detail views= params= bg= n=` splits it, and reports only when the
block costs at least a millisecond so the quiet frames stay quiet.
Co-Authored-By: Claude Opus 5 <[email protected]>
The first Model→Layout switch costs ~9.2 s, and the largest single piece
of it is `view` at 5810 ms — iced widget-tree construction, not the scene.
The scene work that follows it in `prepare` (two `wires-build` calls plus
`frame-prepare`) accounts for 3377 ms, and the log's ordering shows the
two are sequential, not nested: `view` prints before any of them.
`view` being one number could not say which part of a 1950-line
`view_main` that is. It now reports eight sections — viewport_3d,
model_input, grid, selection_overlay, viewport_mouse, viewport_stack,
active_vp_rect, chrome — plus the preamble as `head`.
The marks go through a closure over a `RefCell` rather than one binding
per section: two of the sections sit inside a nested block, where
per-section bindings are out of scope by the time the report runs.
Co-Authored-By: Claude Opus 5 <[email protected]>