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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Solids in a BIM export collapsed onto the origin — every one the right shape,
all in the same place.
ACIS keeps a body's geometry in its own local frame and records where it sits
in a separate `transform` record. Both paths that drew ACIS before this
applied it; the kernel path I put in their place did not, so every solid drew
at its local origin. A file authored in absolute coordinates carries no
transform and looked fine, which is why it took a placed one to show.
The mesh now goes through `finalize_mesh`, which already applied the placement
for the fallback sampler and already split positions into the coarse and fine
pair. That fixes the fault and deletes the hand-rolled copy of the splitting
next to it.
The convention is the part worth testing and the part that invites a mistake:
ACIS treats points as row vectors, `p' = scale·(p·M) + T`, so the stored 3×3
is indexed transposed from a column-vector multiply and the translation is not
scaled. Getting the first wrong mirrors a solid instead of moving it; the
second puts it at twice its offset, which reads as a plausible position and is
the wrong one. Both are covered.
Co-Authored-By: Claude Opus 5 <[email protected]>
The feature existed for one reason: truck's mesh and boolean crates reach
`vtkio → xz2 → lzma-sys`, a C library that cannot cross-compile to wasm32. So
the web build dropped it, and with it the Model tab, solid tessellation and
ACIS import — all of which quietly did nothing in the browser.
cadkernel is pure Rust and has no C dependency, so none of that holds any
more. The feature gated exactly two things by the end, `acis_export` and the
save-path sync that calls it, and both are as portable as everything around
them. Removing it means the web build makes primitives, runs booleans, draws
ACIS solids read from a file, and writes them back out as exact geometry — the
same as the desktop build.
Verified against all three: native, `--no-default-features`, and an actual
`wasm32-unknown-unknown` check rather than an assumption about one.
The docs claimed otherwise in four places and are corrected. `tessellation.md`
in particular was organised around whether an entity "goes through truck",
which is no longer a question anything can be asked — the three paths are now
a kernel B-rep mesh, a curve sampled through `entities::curve`, or geometry
emitted directly, and the per-entity table is updated to match.
`index.html` no longer passes `data-cargo-no-default-features`; there are no
features left to turn off.
Co-Authored-By: Claude Opus 5 <[email protected]>
Two things the truck removal broke without saying so, both found by auditing
what is left in the manifest rather than by anything failing.
The cfg gate on `acis_export` ended up on `acad_to_render` instead, when that
module was put back after being deleted by mistake. `acad_to_render` is what
every entity converts through, so gating it left the wasm build — which is
what `--no-default-features` stands in for — unable to resolve it from thirty
places. It built at the commit before the migration and builds again now.
And deleting TRUCK_CHORD_FRAC took the constant but left its doc comment and
its `#[cfg]` behind, so the attribute landed on the next constant down and
gated a boundary-sampling tolerance that has nothing to do with solids.
lzma-sys was the last dependency in the manifest with nothing using it: no
code references it and nothing else in the tree pulls it in. The `solid3d`
feature it hung off still gates four places, so the feature stays.
Everything else declared is used. `getrandom` and `iced_widget` look unused
by grep and are not — the first is declared to turn on the feature the wasm
target needs from a crate half the tree pulls in, and the second is a
`[patch]` entry rather than a dependency at all.
Co-Authored-By: Claude Opus 5 <[email protected]>
The last four truck crates are gone from the manifest. What they were doing
falls into three parts, and each now has one answer instead of two.
NURBS evaluation. truck was carrying B-spline curves and surfaces for the
SPLINE entity, hatch boundaries, BLEND's endpoint frames, the spline preview
and ACIS spline-surface faces. The kernel now has both, over one de Boor
written across however many coordinates a control point holds — so a plane
curve and a space curve cannot drift apart, and a surface is the same
algorithm applied twice. The rational and the polynomial cases stop being
separate types: weights absent means polynomial.
The entity conversion. Every LINE, ARC, ELLIPSE, SPLINE and polyline was
built into truck topology purely so it could be sampled back into points.
They are sampled through `entities::curve` now, which is where each one's
geometry is already defined once and what EXTRUDE and REVOLVE read — so a
circle drawn on screen and a circle handed to the Model tab come from the
same definition. That retires four of TruckObject's variants and the module
is renamed for what it does.
SWEEP and LOFT. Both only ever produced a mesh, so both are built from point
lists: a band of quads per span, and a lid where a profile closes. Lofting
profiles of different densities resamples them by distance rather than by
index, so a circle lofted to a square no longer twists.
Two things worth noting for anyone reading the old comments. The tolerance
globals that lived in the tessellation module were never about truck at all —
they are the per-frame chord height, and they move to `curve_tol`. And the
rule that a profile handed over as `Lines` silently broke EXTRUDE and REVOLVE
no longer holds: those read the curve directly and never look at this channel.
The two `automation` test failures are unchanged from before this and are not
caused by it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Every 3-D solid OCS makes or opens now goes through cadkernel, which holds
analytic surfaces — a cylinder is a cylinder record, not a spline that looks
like one. That is what lets a solid built here be written back out as exact
ACIS instead of a triangulation, and a facetted export is a one-way door: the
next application to open the file would have no way back to the circle.
The Model tab's seven primitives, the Design-group booleans, 3DROTATE,
3DMIRROR, 3DALIGN, SLICE, SECTION, POLYSOLID and PYRAMID all come from the
kernel now, and `scene.solid_models` holds a `Body`. A mirrored solid needed
its own care before; the kernel reverses the loops on the way through, so it
comes back the right way out rather than lighting black.
EXTRUDE and REVOLVE sweep the profile `entities::curve` already defines, so a
circle, an arc-bulged polyline and a closed spline arrive as the same thing.
A circle extrudes into four cylinder patches rather than a run of chords, and
a profile turned about an axis becomes a cone, a sphere or a torus. A spline
profile has no analytic side wall and is refused rather than approximated.
ACIS goes both ways through the codec's own bridge: `lift` to read and
`append` to write. That drops the 679-line truck conversion and the planar-
only exporter, and the tessellator keeps its bespoke sampler as a fallback for
whatever the kernel cannot yet express — reported rather than passed off as
whole, since a mesh missing a wall looks finished.
truck-shapeops is no longer used and is gone from the manifest. truck-modeling
stays for now: fourteen files use it as a 3-D NURBS evaluator, which the
kernel has no answer for yet, and SWEEP and LOFT have no kernel operation
behind them either.
The two `automation` test failures are unchanged from before this and are not
caused by it.
Co-Authored-By: Claude Opus 5 <[email protected]>
A DIMENSION references the block "that contains the entities that make up
the dimension picture", and that block is the picture: AutoCAD requires
it and draws it, BricsCAD draws it when present and only renders from the
dimension variables when it is missing. OCS re-derived the picture from
DIMVARS every time and ignored the block entirely.
That works while a style agrees with what the drawing actually drew. A
Revit-exported schedule here does not: its style stores DIMTXT already in
drawing units beside a DIMSCALE meant to scale it, so the text came out
three hundred times too tall, and each dimension carries a DIMFXL
override that is likewise already in drawing units, so the extension
lines ran nearly a million units past the points they measure.
Neither is guesswork about units now. The block holds the geometry the
drawing was authored with, so it is drawn: the contents are already in
world space and the block's base point is the origin, and the identity
instance that carries them exists only to reuse the scene graph's
ByBlock / layer-0 colour resolution. Every wire answers to the dimension,
so a pick still selects the dimension rather than one of its pieces.
Re-deriving stays as the fallback for a dimension with no block.
Annotative dimensions keep the old path: their several representations
are separate blocks and choosing between them is what the annotation
machinery already does, while the doctrine above assumes one picture.
A picture is only good while its inputs hold, so it is dropped when they
change: editing a dimension's properties, and saving the dimension style
it is drawn with. Entity edits already dropped it.
Co-Authored-By: Claude Opus 5 <[email protected]>
A revolution comes back from rsweep as three 120-degree segment faces,
but every hole was attached to the first of them. A window wider than a
segment cannot sit on one face, so that face turned invalid and its
whole third of the tube vanished -- the pump's cylinders lost 120
degrees of wall each and the solid read as open where they met the body.
One of them shed a segment with no hole involved at all, so chasing the
individual causes would not have covered it.
Measure the result instead: after a cone face whose boundary closes the
revolution is triangulated, check that the meshed vertices actually span
it, and hand the face to the parametric sampler when they do not. That
catches a tear whatever produced it, while genuinely partial cones and
every non-cone surface pass straight through -- the tee's pierced run
cylinder still meshes through the kernel.
Also bring truck's chord tolerance in line with the detailed LOD. Truck
emits a single mesh rather than an LOD ladder, so a tenth of the radius
left a wide pipe as a hexagonal prism whose flats sank nine units inside
the true surface, pulling the wall away from the caps and planar faces
that meet it.
Co-Authored-By: Claude Opus 5 <[email protected]>
The cone-surface record's trailing real is a parameter scale, not
geometry. It equals the radius on many bodies, so cylinders generally
came out right, but where the two diverge the lateral surface was built
at the wrong size while the flat caps stayed on their own rims -- a
gauge dial drew as a ring around its own face, its band at 43.2 against
caps at 34.56.
The major axis is the radius vector at the reference cross-section, so
its length is the radius. Route every read through one helper and keep
the record's field only as a fallback for a degenerate major axis, so
the band, the silhouette generators, the isolines and the coaxial-rim
check all size themselves the same way. Taper is unchanged: the major
axis is still the h=0 radius that radius + h*tan(a) extends.
Co-Authored-By: Claude Opus 5 <[email protected]>
Sphere faces drew as complete balls. ACIS parametrises a sphere about
any pole, so a vessel's dished head is commonly bounded by a circle that
is no line of latitude in that frame, and a theta/phi bounding box of
such a boundary covers nearly the whole ball. Derive the window
analytically instead: when a face's loops are circles lying on the
sphere, re-pole the frame onto their common axis, where each seam is an
exact latitude at acos(d / R). One ring bounds a cap, two bound a zone;
the material side comes from the boundary winding, reusing the helper
the torus path already had.
Separately, the longitude wrap flag was resetting the colatitude window
to the whole meridian. A cap centred on the pole covers every longitude
while still ending at its seam, so that reset grew every cap back into a
ball. Trim the two angles independently and rename the flag theta_full
so the split reads at each use, including the silhouette generator,
which skipped the colatitude test the same way.
Cone faces took their height span from the first boundary loop alone. A
lateral face closed off by one rim and one pierce curve -- a pipe branch
meeting its run -- carries its two ends on different loops, so the tube
was sized to whichever came first and the leg above the junction went
undrawn. Read every loop.
Co-Authored-By: Claude Opus 5 <[email protected]>
Route model, paper, viewport and nested block content through one traversal while preserving leaf renderers. Carry decoded material state into mesh shading and harden solid geometry conversion.
Scope revolution spans to face boundaries and preserve pierced cylinders and rational spline lofts so valid bodies no longer produce stretched or missing shells.
Preload one language-aware font resource across the interface, drawing text, and navigation labels.\n\nRender the navigation cube, ring, outlines, and labels through a 4x MSAA target.
Two rendering defects surfaced on the budweiser benchmark drawings, both
tied to a drawing's annotation scale.
Annotative scale representations. A drawing may materialise each annotation
scale as its own object (here a block insert per scale, on a "0 @ <scale>"
layer, each carrying a per-object AcDbContextData bound to one scale).
Nothing filtered them, so every scale drew at once — a 1× copy stacked
under a 10× one. Add annotative::annotative_offscale and apply it in the
three paths that expand block content: the wire block-defn cache, the
resident/top-level visibility gate (model space only — paper viewports use
their per-viewport frozen scale layers), and the exploded-insert hatch/fill
walk (filtered before normalize strips the handle). An object is shown when
a representation matches the current scale; the off-scale ones are hidden.
When the current scale is unsupported by any representation, fall back to
the base "1:1" copy instead of hiding everything (the object must not
vanish) or showing everything (the copies must not stack).
Model-space linetype scale. PSLTSCALE (a paper-space flag) was applied to
model-space linetypes via the annotation scale, so a drawing saved at e.g.
CANNOSCALE 10:1 drew its linetypes 10× off. Thread a `paper_space` flag
through tessellate_entity and gate the PSLTSCALE factor on it, so it only
scales linetypes for content shown inside a paper-space viewport; model
space keeps LTSCALE × CELTSCALE.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A circle with a complex linetype (embedded shapes/text, e.g. IZOLACE) drew
its dashes and symbols scattered far off the entity at UTM-scale
coordinates. Two defects compounded, both catastrophic-cancellation at a
~1.2M-unit origin:
- The single-entity complex-linetype path fed apply_along the wire's HIGH
double-single half only (`base.points`), an absolute f32 quantised to
~0.1 at 1.2M. Walking the pattern along those points blew the shape /
dash positions up to ~1e308. Reconstruct the absolute f64 path from
high+low, re-origin it at the first vertex, walk apply_along in that
local frame, then lift each result wire back to world with
shift_wire_to_world (now pub(crate)) — the same local-frame trick the
MLINE complex-linetype path already uses. Fixes every entity, not just
circles.
- The circle was tessellated with truck's circle_arc (arc-through-three-
points), whose fit cancels catastrophically at 1.2M and returned a ~3%
radius wobble with uneven segments. Tessellate directly as a cos/sin
polyline (adaptive segment count from the curve tolerance); adding a
small ±r term to the centre stays precise, and the Lines path RTE-splits
it. Arc and polyline bulges share the circle_arc root and remain to fix.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Three defects made a leader's arrowhead render as one stray line far
longer than the leader itself, at UTM coordinates:
- The arrowhead was drawn even when it is larger than the first leader
segment it sits on. A DIMASZ sized for a bigger plot (here 6 units on a
~1-4 unit pointer) painted a stroke longer than the whole leader.
AutoCAD omits the arrowhead when it does not fit the first segment, so
gate the arrow on `arrow_size <= first_segment_len`.
- Leader geometry was built as absolute f32. At UTM coordinates (Y ~ 4.5M,
f32 ulp ~ 0.5) the oblique arrow tick's small cross-slope quantised
away, collapsing the 45-degree tick into a flat horizontal line. Build
the path and arrow in absolute f64 — the arrow around a local origin so
append_arrow's f32 math stays precise — then RTE-split into the double
single (points_low / fill_tris_low) at the end.
- The `_Oblique` / `_Archtick` arrowhead block mapped to ArrowKind::Tick
with the full DIMASZ, but the tick emitter draws `size` to either side
(half-length semantics, matching DIMTSZ). Pass half so an oblique
arrowhead sizes to DIMASZ like every other arrowhead, not double.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The wipeout pass ran after the wire pass with no draw-order bias, so
the mask erased its own boundary frame (and everything else at its
plane regardless of draw order). The mask uniform now carries the
entity's draw-order depth (uniform grows 80 → 96 bytes) and biases its
clip-z like every other pipeline, and the frame wire rides a
+half-rank override — the later mask pass fails the depth test at the
frame's pixels, so the boundary stays visible, while entities drawn
above the wipeout are no longer erased.
Co-Authored-By: Claude Fable 5 <[email protected]>
The embedded solid display-cache wires are body-local (unplaced), and
R2013+ AcDs-backed solids used to misparse them into garbage points near
the origin — a clipped region rendered a stray fragment at (0,0,0). When
the ACIS payload parses, the mesh pipeline (fill + feature edges +
isolines, all body-placed) is the authoritative render, so the wires now
only serve as a last resort for unreadable ACIS data.
acadrust bump brings the decoded wireframe section and real reference
points: solids/regions report the file's stored bounding-box centre (or
a geometry-derived one) instead of (0,0,0), which feeds OCS grips,
Position properties and insertion snaps.
Co-Authored-By: Claude Fable 5 <[email protected]>
Clipped INSERTs cut their content correctly but never showed the
boundary itself. When the header XCLIPFRAME is 1 or 2 (visible), a
closed ring over the world-space clip polygon now rides the insert's
resolved colour and line weight. The frame is deliberately not clipped —
it is the boundary — and carries no snap points.
Co-Authored-By: Claude Fable 5 <[email protected]>
The SAB reader tokenizes an ACIS transform record's matrix rows and
translation as Position triplets, while SAT text yields 13 individual
floats. body_transform() collected only float tokens, so every binary-SAB
solid/region (R2013+ AcDs payloads) lost its placement and rendered at
the body-local origin. Flatten Position tokens into the numeric run.
Co-Authored-By: Claude Fable 5 <[email protected]>
Xref merge now inserts entities in the source block's chain order and
carries SortEntitiesTable objects across (remapped to merged handles).
draw_depth_map values become [depth, half] so exploded block/xref fills
and wide-polyline band wires both compose child depth as
insert_depth + in-block rank x half, recursing into nested inserts with
the same divisor - bands and fills from one block interleave by the
block's internal order instead of stacking at the insert's single depth.
Band batches split per rank via WireModel.depth_override; thin wires
stay merged. The wire arena falls back to a full rebuild when a slab
carries mixed depths so refreshes don't flatten band overrides.
Co-Authored-By: Claude Fable 5 <[email protected]>
Images, OLE frames, wipeouts, underlays, paper-space viewports and
drawing-view borders were selectable only on their outline. Each now
carries an interior pick surface (pick_tris over its rectangle, or the
triangulated clip polygon for a clipped underlay), ranked below edge
and fill hits so content drawn inside still wins the click. The
layout's own sheet ("overall") viewport is excluded — identified by
the Layout object's viewport link, since DWG files carry viewport
id = 0 and some center the sheet viewport off-origin — or it would
swallow every click over the real viewports beneath it. Drawing-view
borders draw nothing normally; they emit an invisible pick-only wire
whose rect outline appears while selected.
Co-Authored-By: Claude Fable 5 <[email protected]>