Advancing ocad.pgp.version (or the web counterpart) could succeed while the
merged alias table write failed, so a later launch skipped migration against a
stale file and permanently lost the new aliases. Gate the version write on the
alias-table write (native fs::write / web set_item) succeeding.
Existing installs only got newly-added default aliases when storage was
absent; load_aliases parsed an already-present ocad.pgp or web alias table
verbatim, so R/RA/RE/REA remained unknown to profiles seeded before version 2.
Introduce a versioned default merge: track the default-table version each
profile has seen (wordless ocad.pgp.version marker / localStorage key) and
fold in only the aliases introduced after that version, never overwriting
user-defined mappings or resurrecting deliberately removed aliases. On first
migration the merged table and the advanced marker are persisted so later
launches skip the already-delivered aliases.
The shaded surface of a curved solid was visibly coarser than its own
wireframe: the wire ran along the true rim while the facets cut inside it.
Two things were wrong, and only one was the tolerance.
`facet_res` is a resolution multiplier — the same one `scale_lod` divides the
fallback sampler's chord fraction by — and the kernel path was using it as a
sag in world units. At the default that asks for a whole unit of departure, so
nothing smaller than a metre subdivided at all and a face was only ever as
round as its own boundary. It is a fraction of each surface's own radius now,
which carries no assumption about the drawing's units.
And it is the *edges'* fraction, shared from `EDGE_CHORD_FRAC` rather than
copied. Those edges are drawn over these faces; sampling the two differently
is what put the wire across a facet instead of along its corners. `facet_res`
is deliberately not applied either, since the edges it must line up with are
built once at highest detail and never scaled.
A torus is measured by its tube rather than its ring — the tube is the tighter
bend, and sampling to the ring leaves the section a hexagon.
The primitive-volume test claimed a mesh can never read over its true size.
That holds for a convex solid, where every chord lies inside; a torus is not
one, and across the inside of its tube the chords fall outside the material.
It passed before only because the sampling was too coarse to show it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round things came out with too few sides in drawings measured in metres, and
zooming in did not help.
The chord tolerance was floored at five thousandths of a world unit. That is
a fine sampling for a part measured in millimetres and a coarse one for a
building measured in metres: a ten-centimetre pipe got about ten sides and a
five-centimetre bolt about seven. The floor was also what stopped the frame
asking for better, so closing the camera on one changed nothing.
A chord height is an absolute length, so any fixed value carries an assumption
about the drawing's units. Bounded against the curve's own radius instead,
which carries none. The two ends are really segment counts: a circle gets at
least about fifty sides and never more than about a hundred and sixty,
wherever the camera is and whatever the drawing is measured in. Between them
the frame still decides, so a curve fills in as the camera closes on it.
Straight runs keep the frame's own tolerance — there is no curvature to
sample, and two ends are all a line has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A drawing placed at UTM shimmered as the camera moved.
The relative-to-eye path exists so the large eye translation never enters an
f32 matrix, and it zeroed the translation column to make sure. But the basis
was built first, by `look_at(eye, target)` with both positions cast to f32 —
and at (639792, 4517057) that grid is half a metre across in y. Rounding both
ends and then subtracting them left a direction that changed as the camera
moved, so the whole view basis jittered. Zeroing the translation afterwards
came too late to help: the damage was in the rotation.
It never needed the positions. The eye is `target + rotation·Z·distance`, so
the direction between them is the rotation's own Z — a unit vector carrying no
coordinate at all.
Two tests. Where the camera stands must not reach the matrix, which is the
property being restored; and at the origin, where the old construction had no
large coordinates to lose, the two must agree exactly — which is what says
this kept the same basis rather than turning the view round.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Faces were being dropped for sitting a nanometre off their own plane.
An edge is shared by two faces and in a real file cannot lie exactly on both:
the two surfaces were fitted separately and written to finite precision. Asked
for exactness, the kernel decides the edge is not on its plane, declines to
project it, and the face goes undrawn — silently, since a face that cannot be
expressed is reported as absent rather than as an error.
A micrometre in a drawing measured in metres recovers twenty-six walls of one
building. Loosening further buys almost nothing — a hundredth of this finds
one more face in sixty thousand — and past that it would start accepting
geometry that really is wrong, so it stops here.
With the tube fix that came with the kernel bump, VILLA_NEU_IFC3.dwg now
meshes 59015 of its 59033 faces, from 58922.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two faults kept faces out of a lifted solid, and both looked like a document
problem rather than a reader one — the lift reported no loss at all.
An edge's stored parameter pair can span the right length from the wrong
place, putting a straight curve beside the face it bounds instead of along
it. And a loop's coedges are not always listed in the order the ring runs,
which the old chaining could not fix: it reversed the next piece in the list,
where what was needed was a different piece.
Either one leaves a ring that crosses itself, which ear clipping declines, so
the face is dropped without a word.
Handle 176 of Annotation-2D-Mesh-Solid-BIM.dwg now meshes all fourteen of its
faces, where it managed ten. Across that file: 105 of 107 solids complete, 865
of 867 faces. Across VILLA_NEU_IFC3.dwg: 672 of 763 solids, 58922 of 59033
faces.
The fallback sampler stays until that reaches all of them — a complete mesh is
what stops it running, and 111 faces still need it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A face with a hole came out with the hole solid and the material around it
missing. Handle 176 of Annotation-2D-Mesh-Solid-BIM.dwg is one.
`tess_plane_face` read `face.first_loop()` and fan-triangulated it, on the
grounds that holes were the kernel path's job. Two things make that wrong.
Nothing says the outer loop is listed first, so when a hole came first the
hole got filled and the boundary was ignored. And the kernel path does not
always win: when its mesh is incomplete, `tessellate_acis` runs both and keeps
whichever produced more triangles — which is not a measure of being right.
Every loop is read now, flattened into the plane's own frame, and triangulated
with the widest ring bounding and the rest cutting into it — the same rule the
kernel uses, and by area rather than by listing order.
This is interim. The fallback should not exist at all: the kernel already
lifts these bodies with no loss and meshes ten of handle 176's fourteen faces.
The four it drops are planar rectangles whose boundary projects with one
edge's `v` mirrored, so the ring self-crosses and ear clipping declines it —
a `pcurve` fault, not a missing feature. Fixing that makes the mesh complete,
and a complete mesh means the fallback never runs and can go.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Faces with holes were drawing the hole filled and the material around it
missing. Handle 176 in Annotation-2D-Mesh-Solid-BIM.dwg is one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>