Splitting a boundary object erased it and added the pieces, so the hatch
lost that source and rebuilt from what was left, often the whole circle.
The pieces now take over the old object's association before it is
erased.
Fixes#1572
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A point carried a key vertex, so Endpoint (which outranks Node) claimed
it, and an absolute-size glyph's strokes were indexed for Nearest and
Intersection. Points now offer only their Node, in the indexed and the
unindexed snap paths alike.
Fixes#1570
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each band segment was a rectangle ending square at its vertex, so the
outer corner of every joint showed a notch. Every segment end now carries
the tangent of half its turn, and the shader slides that end's corners
onto the joint's bisector so neighbours meet in a miter. Turns sharper
than the limit keep butt ends. Constant-width runs inside a polyline split
around its arcs no longer count as tapered, which had kept them unmitred.
Fixes#1565
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ensure_layer had started trimming the name, so an entity on " A" got a
table entry "A" it never resolves to. Register the name as given again
and drop the unused return value. The five copies of the dirty-flag check
become sync_registered_layers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR pinned codec and kernel to its fork through [patch]; codec #77 and
kernel #73 are merged upstream, so the patch sections are replaced by
the upstream pins (codec 8d23408, kernel d22a270, graph 9b5ae39) in this
same commit: the kernel now applies the body transform during the lift,
and an OCS without this PR's change would apply it twice. The new codec
also brings AssocCurveValue, registered with the plugin API's type
registry, and EmbeddedEntity::Body, named as a polyline profile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR pinned the codec to its fork through [patch]; codec #75 is merged
upstream and main already points at it, so the patch section is dropped.
The two windows-sys features it adds (Globalization, Identity) stay.
The PR pinned codec and kernel to its fork through [patch]; codec #76 and
kernel #71 are merged upstream and main already points at them, so the
patch sections are dropped.
#1580 built a GlyphSource for every fallback glyph, so drawing a CJK
character on screen copied the whole fallback font (tens of MB) into a
process-wide cache whether or not a PDF was ever exported. The source is
now built only when fallback_source asks for it, through the same search.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts with 02c470aa (invisible searchable text layer): main already
depends on allsorts 0.16 with the same features, so its line stays. Both
layers would put the text into the PDF twice, so a run drawn wholly in
faces the PDF can embed now goes out as embedded-font text and skips the
invisible layer; any other run (stroke fonts, mixed scripts) keeps its
outlines and the invisible layer. Faces only probed for such a run are
not installed. The PR's test follows layout_glyph_quads' new tuple.
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]>
Conflicts with #1573: the locale files and the catalog take both PRs'
entries (one "Insertion Point" mapping kept); the Home and Insert Block
panels take #1574's layout with #1573's Insert Block gallery.
Real BT/Tj text objects ride alongside the vector outlines (Tr 3,
zero visual change): runs preserved at layout time, TTF styles embedded
as allsorts subsets with ToUnicode, width-matched Tm, clip-culled.
WireModel literals moved to ..Default::default() tails.
Codec: PRs #71-#74 merged (MPOLYGON layout, multiline ATTDEF/ATTRIB DXF,
DXF MATERIAL/ACSH matrix, restorable SAT/SAB + history roots) and issues
#55 #64-#69 fixed. EntityType now boxes Surface, Extended, Table,
MultiLeader and Helix (2344 B -> 848 B per entity): construction sites
wrap in Box, the entity traits forward through Box, and the python
plugin generator converts with into(). Kernel: PR #70 (cuboid ring
winding).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PDF export filled every TrueType glyph's tessellated outline, one triangle
per triple. A stroke-heavy CJK face such as DFKai-SB tessellates into
hundreds of triangles per character, so a 28-layout schematic set
exported at 320.9 MB and viewers crawled through it. The atlas export
table was also rebuilt for every text wire, which made the same export
take 566 s.
- ttf_glyph: GlyphSource records the face, glyph id and font-unit scale a
filled outline came from; glyph_source / fallback_source resolve the
same face as glyph / fallback_glyph (fallback picks both in one search).
- Face::glyph_source mirrors Face::glyph; stroke glyphs (LFF, SHX, big
font) return None and keep their polyline export.
- GlyphExport carries the char and the source (filled glyphs only, so
TEXTFILL 0 still exports hollow outlines).
- pdf_export draws a sourced glyph as text (Tf 1 + a Tm that maps its em
onto the SDF quad), subsets each face with allsorts after all pages
(keeping cvt/fpgm/prep for tricky fonts), renumbers glyph ids, and
snapshots the glyph table once per export.
Same set: 566 s / 320.9 MB -> 27 s / 1.42 MB; text is searchable and
lands on the same pixels as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A raster reference that cannot be resolved is the only framed entity whose
to_render emits a RenderObject::Text: the frame and its diagonals are lines,
but the saved file path becomes a real glyph run in the SDF text buffer.
gather_text_verts reuses the whole previous vertex buffer when
text_unchanged(epoch) says no text-bearing entity moved, and that asks
entity_affects_text_cache -- which enumerated Text, MText, Dimension,
MultiLeader, Leader, Table, Tolerance, attributes and Insert, but not
RasterImage. So an erase never raised CACHE_CATEGORY_TEXT, the stale buffer
was taken as valid, and the path kept drawing over empty space. The reuse
branch then re-stamps that buffer with the current epoch, which froze it
until a full rebuild. CACHE_CATEGORY_IMAGE did not cover the gap: it is set
from images.contains_key(handle), and a missing reference has no ImageModel
at all.
The same gap broke three more flows from the Added/Modified side: ATTACH of
a missing reference showed no text, MOVE and grip-drag left the text behind
while the frame followed, and UNDO after an erase did not bring it back.
entity_affects_text_cache now counts a RasterImage whose reference does not
resolve, reusing the converter's memoised resolve_image probe so the two
cannot disagree. Kept a probe rather than a blanket type match so a healthy
image does not rebuild every glyph in the drawing on each mouse move of a
grip-drag.
Four regression tests, each confirmed to fail without the fix. The main one
is end-to-end on the buffer the GPU draws: before, the erased placeholder
left 300 glyph vertices on screen (50 chars x 6).
Also: to_render ran the same resolve probe twice; now once, shared by the
outline and placeholder branches. Moved text_unchanged's doc comment off
draw_ranks_stable, where it had drifted, and recorded the rule that every
entity able to emit a GlyphRun must be declared there.
Co-Authored-By: Claude Opus 5 <[email protected]>
Two viewport-navigation defects, both measured against commercial solutions.
The cursor stayed a hand during a zoom. `selection_overlay` took one boolean
for all three interactive navigation tools, so ZOOM Dynamic and ORBIT both
showed PAN's grab hand — which reads as "drag the sheet", the wrong promise
for either. Split it into a `NavCursor` enum: PAN keeps the open/closed hand,
ORBIT gets four-way arrows, and ZOOM Dynamic gets the magnifier carrying the
sign of the zoom actually under way — "+" dragging up, "-" dragging down,
latched with a 2 px dead zone so a hand held still cannot strobe the glyph.
ZOOM Dynamic also reused `box_anchor` to remember where a drag began, so a
selection marquee rubber-banded across the screen while the view zoomed; the
drawing is now suppressed, the state left alone for `reproject_box_anchor`.
ZOOM EXTENTS did not reach the whole drawing, for three separate reasons, now
answered by one shared `Scene::visible_block_bounds`:
- A quartile filter dropped any wire whose centroid sat outside the
inter-quartile consensus of the others. It was meant to stop one junk wire
from a bad parse poisoning the box, but it could not tell parser junk from
an outlying detail, a survey point, or a block placed away from the rest,
and silently refused to frame them. Gone; the absolute 1e16 sentinel gate
the paper path already used is the only filter, and the XLine / Ray display
segments that motivated the heuristic are handled explicitly.
- HATCH tessellates to no wires at all, and the model path never read the
fill models, so a drawing whose outermost geometry is a fill — or which is
nothing but fills — had nothing to frame. It now folds them in, reusing the
renderer's own warm cache on the hot path rather than re-exploding every
INSERT per ZOOM EXTENTS.
- Inside a floating viewport the fit came from `compute_model_space_extents`,
which walked only `key_vertices` — empty for every tessellated curve.
Circles, arcs, ellipses and splines contributed nothing. Fixing it also
corrects plot Extents, sheet-set framing, viewport auto-fit and SpaceMouse
model bounds, which all inherited that blind spot.
Bounds stay f64 into the camera: a UTM drawing lost whole metres to the old
f32 round-trip. Drawing extents also no longer read the camera — the result is
cached per geometry epoch, and a floating viewport's camera is derived FROM
the extents, so `camera_for_viewport` recursed into a stack overflow.
`model_space_extents_mixed_entities_accuracy` asserted the curve-blind result;
it now asserts the true bounds.
Co-Authored-By: Claude Opus 5 <[email protected]>
Attribute Definition dialog and Edit Attribute Definition dialog, -ATTDEF
with the reference prompts and modes (AFLAGS), tag shown in the drawing,
justification-aware grips, reworked Properties with validation, Home Block
slide-out and Insert Block Definition panel, and the codec patch for
ATTDEF DXF output and the R2013+ solid wire cache.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rebuild the Blocks palette with Current Drawing / Recent / Favorites /
Libraries tabs, wildcard filter, six views, insertion options, click and
drop insertion and a block context menu. INSERT opens the palette, the
new -INSERT runs on the command line, the ribbon Insert button becomes a
block gallery, and BLOCKMRULIST, BLOCKREDEFINEMODE, BLOCKNAVIGATE,
BLOCKSTATE, EXPLMODE and INSNAME are added.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Block children of one style are batched into a single wire, and a
plinegen wire carries its dash distance across the joins. Every LINE
wire is plinegen, so each line in a block began mid-dash wherever the
previous one had stopped. PLINEGEN governs a wire's own breaks; a member
without any now enters its batch as non-plinegen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Once edited, a dimension is drawn live instead of from its *D block;
on the #898 drawing that differed in three places:
- Oblique / architectural-tick strokes were DIMASZ/2 to each side, the
block's are DIMASZ/sqrt(2) (its unit stroke runs corner to corner).
- Arrowheads and ticks shared the dimension line's wire, so a dashed
dim line broke its ticks into stubs. They get their own solid wire
(and explode as lines as before).
- A diameter's user-positioned text was lifted by the DIMTAD offset even
when the stored point already was the text's middle, as a file saved
elsewhere writes it; it is lifted only when it lies on the leader
(#1323's case).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Bug:
$ISOLINES (header i16) and Surface.u/v_isolines (file-controlled i16)
were only ever floored at 0, never capped, and were handed to the
tessellation kernel as the per-face isolate count - which draws
0..count isolines per face with no ceiling of its own. SETVAR ISOLINES
32767 (or a DWG carrying that header, or a Surface with u_isolines =
32767) turned every rebuild and every per-cursor preview of
PRESSPULL/REVOLVE/SWEEP/LOFT into a multi-million-vertex computation:
measured 4,259,966 edge vertices for one 12-unit cylinder preview.
Reproduce:
1. SETVAR ISOLINES 32767 (or open a DWG whose $ISOLINES is large, or
edit a Surface whose u_isolines is 32767).
2. Draw a circle and start REVOLVE (or PRESSPULL/SWEEP/LOFT), or shade
any 3D solid.
3. Before: the app allocates without bound - abort on memory failure or
a permanent freeze; previews recompute on every mouse move.
4. After: previews stay interactive; a hostile header renders exactly
like the largest value the Options slider can request.
Fix:
Cap each value at the ceiling this product already agrees on, where the
type changes from i16 to the kernel count: header $ISOLINES <= 64 (the
Options slider's own "Isolines per surface" range), Surface
u/v_isolines <= 200 (the plugin editor's writable range). One helper
(clamp_header_isolines) replaces all 11 copies of .max(0) as usize
across the entity/scene/control/display feed sites,
surface_isoline_counts clamps both axes, and SETVAR ISOLINES stores
x.clamp(0, 64) so the echo, slider and re-saved files agree with what
renders; file-sourced headers keep their raw stored value and are
clamped at consumption. Clamping is density-only: geometry, bounds and
saves are untouched. Three regression tests cover hostile surface
counts, the SETVAR entry, and a file-sourced header measured through
display tessellation (hostile renders like the slider ceiling, negative
renders like zero).
Bug:
The DGN compound-file reader walked FAT chains with a fixed
1,000,000-iteration guard - a step budget, not a byte budget. A
self-referential FAT entry (fat[s] = s) burns the whole budget reading
the same sector: 4.1 GB per call at 4 KiB sectors. Directory, mini
stream and miniFAT chains are alive simultaneously (about 12 GB peak) ->
allocator abort while merely selecting the file in the attach dialog.
Separately, models() inflated every model directory's streams (512 MB
each) with no total, and directory entries can all point at one shared
payload - 1.2 GB retained from a 4.3 MB file.
Reproduce:
1. Craft a ~1.5 KB CFB (sector shift 9) whose directory start sector's
FAT entry points at itself (zeroed FAT sector).
2. ATTACH and pick the file (any listing path parses the CFB).
3. Before: the directory chain grows to 4,000,000 entries from a
1536-byte file, then abort.
4. After: every chain is bounded by the file's own sector count -
instant, memory at most the file size.
5. For the models path: a file with 300 model dirs sharing a 4 MB
payload inflated to 1,258,291,200 bytes before; now it stops at the
file-scaled budget (max(file x 64, 256 MiB)).
Fix:
chain bounds itself by file bytes / sector size: a chain can only read
in-file sectors, and after that many steps it must have repeated one -
the walk is deterministic, so a repeat is a cycle that yields nothing
new. That is the strongest bound the format allows and it never
truncates a legitimate chain (each sector is read at most once). The
mini-stream loop uses the same argument over mini.len() / 64. models()
checks a file-scaled total budget before inflating each dir; later dirs
are dropped (earlier ones still load, name/table discovery degrades
gracefully). Tests: the hostile 1.5 KB file keeps all bytes <= file
size; the 300-model file stays within budget.
Bug:
defn_inline_point_cost_recursive and defn_metrics_recursive guard against
cycles (a visited set) but not against depth: a long legal chain of
block definitions (A inserts B inserts C, ...) recurses once per level
and overflows the stack. A document with about 20k-deep nesting kills
the process the first time block metrics are computed (opening the
drawing / measuring), even though there is no cycle.
Reproduce:
1. Build a drawing whose block definitions nest deeply - a chain of
20,000 INSERTs, each into the next.
2. Open it (metrics are computed on access).
3. Before: STATUS_STACK_OVERFLOW (0xc00000fd) - process death.
4. After: traversal stops at 33 levels (depth > 32), matching
expand_defn's existing limit exactly, and metrics fall back to the
default for the truncated branch.
Fix:
Both entry points now bail when visited.len() > MAX_NESTING_DEPTH (32)
alongside the existing cycle check - the same depth + 1 / depth > 32
convention expand_defn has used (33 levels), so the two walks agree on
what "too deep" means; the cycle guard stays. The depth test runs first
(O(1)) so deep-but-cyclic graphs still short-circuit. Tests: two
deep-nesting cases that overflowed before now complete.
Bug:
EMF polyline/polygon records carry a ring count as a raw u32.
pts16/pts32 did Vec::with_capacity(count) on it directly - a crafted
4294967295 pre-allocates about 4 billion slots and the allocator aborts.
The existing total > 400_000 guard only checked the sum of ring counts,
never an individual ring's vector.
Reproduce:
1. Craft an EMF whose POLYPOLYLINE/POLYGON count is 0xFFFFFFFF.
2. Attach/import it.
3. Before: "pre-allocated 4294967295 slots" then memory allocation
failed (multi-GB pre-allocation).
4. After: the vector is sized from the bytes actually present in the
record; the parse consumes what the record holds.
Fix:
pts16/pts32 size their capacity as count.min(available_bytes / point_size)
- the record's own remaining length is the ceiling, so allocation is
O(record). The iteration logic is untouched: it stops when the bytes run
out, so output for valid records is byte-identical (the guard is a pure
allocation bound). Tests: the u32::MAX ring no longer pre-allocates;
valid single- and multi-ring records still parse.
Bug:
arc_cubics turned a raw sweep angle from the DGN file straight into a
piece count - ceil(sweep ...) as usize fed with_capacity, and an extreme
sweep (huge, or +/-inf) produced a count that overflows usize
allocation, causing a capacity overflow panic while attaching the file.
Legal DGN arcs top out near 104 radians.
Reproduce:
1. Craft a DGN with an arc element whose sweep angle is enormous (1e18+,
+/-inf).
2. ATTACH it.
3. Before: "capacity overflow" at raw_vec during model build.
4. After: the arc clamps to +/-4096 quarter-turns (sign preserved, NaN
passes through, +/-inf handled) and renders as many cubics as fit.
Fix:
The clamp happens in the one funnel (arc_cubics) before ceil: sweep is
limited to +/-4096 x 90 degrees, far above the legal 104 rad but orders
below what overflows the count. The sign of the sweep is preserved so
arcs keep their direction, and NaN passes through to the existing
empty-arc handling. Tests cover three sweep magnitudes (including
+/-inf) building without overflow.
Bug:
The PDF underlay renderer sized its pixmap from page size x requested DPI
with no ceiling: a large page at a high raster DPI (display or print)
computes a multi-gigabyte image - testing reached a 17 GB pixmap and a
543-second render attempt before the allocator gave up.
Reproduce:
1. Attach a large-page PDF; set a high raster DPI (print above 150 DPI,
or display with DPI raised).
2. Before: minutes of rendering and a multi-GB allocation, then abort.
3. After: display rasters are clamped so the longest side is at most
3072 (like DWF/DGN, the effective DPI is stored); print rasters over
268,435,456 pixels are refused with an error instead of rendered.
Fix:
Display (at or below 150 DPI): the scale is clamped to
RASTER_SIDE/longest before the pixmap is allocated, and the DPI actually
used is stored for downstream math. Print (above 150 DPI): refused when
width x height exceeds MAX_PRINT_RASTER_PIXELS (256 Mpx) or is
non-finite - print DPI is never silently rescaled because the
sheet-mm-from-exact-DPI conversion must stay exact; the user gets an
error instead of a hang. A regression test covers the hostile page x DPI
case (about 1 second, previously 543 seconds and 17 GB).
Bug:
Loading a DWFx package recursed twice on untrusted input:
1. roxmltree's tokenizer (parse_element <-> parse_content) recurses once
per element nesting level (present in versions 0.20 and 0.21) - a
page or descriptor nested about 100k deep overflows the stack inside
Document::parse, before any application code runs;
2. the XPS walk recursed once per nested Canvas with no limit - about
10k levels overflowed the stack.
Either one aborts the process during ATTACH.
Reproduce:
1. Build a DWFx (zip) whose .fpage / descriptor.xml nests elements
(e.g. Canvas) about 100,000 deep.
2. ATTACH it.
3. Before: STATUS_STACK_OVERFLOW (0xc00000fd) - process death; a test
calling only roxmltree::Document::parse on the same bytes reproduces
it, proving the dependency's tokenizer dies first.
4. After: the file is rejected before parse (a log::warn records "XML
nests deeper than 256 levels ... document rejected"), or - for Canvas
nesting between 65 and 256 - the page renders with the too-deep
subtree skipped while siblings and the rest of the page stay.
Fix:
A linear depth scan (xml_depth_within, skipping comments, CDATA, PIs,
doctype subsets and quoted attribute values so their angle brackets
never count; it may over-reject malformed markup but must never
under-count) gates all three Document::parse sites at 256 levels - real
pages nest single digits - and walk caps Canvas nesting at 64 (continue
skips that subtree, siblings unaffected), matching the compound-file
walker's cap. Rejections are logged so a rejected depth is
distinguishable from a corrupt parse.
Bug:
decode_packed (RLE-packed DIB decoding) guarded the header with a
12-byte check but then read fixed fields at offsets 14-16, 16-20 and
32-36. Any header shorter than 36 bytes that took the non-core path
panicked with an out-of-bounds slice - every input in that range.
Reproduce:
1. Open a BMP whose DIB header takes the non-core path but is shorter
than 36 bytes (a 12-35 byte header claiming packed data).
2. Before: out-of-bounds panic at the fixed-field reads.
3. After: the header is rejected and the image skipped; valid headers
(including a 1x1 DIB) still decode.
Fix:
The non-core branch requires len >= 36 before any of its fixed reads -
every input under 36 bytes panicked before the fix, so no non-panicking
behaviour changes. Valid packed DIBs (tests include a 1x1 image) decode
unchanged.
Bug:
The EMF record reader accepts nSize = 28 (header-only) so legal empty
polylines load, but the sub-arm handling those iTypes also reads a u32
count at offset 28 for records that are not empty - rec.get(28..32) on a
28-byte record is out of bounds, panicking while importing the file.
Reproduce:
1. Craft an EMF with an EMR_POLYLINE (or related iType) record whose
nSize is exactly 28.
2. Attach/import it.
3. Before: out-of-bounds panic reading at offset 28.
4. After: the undersized record is skipped; a full 36-byte record still
renders.
Fix:
The sub-arm that reads at offset 28 now requires rec.len() >= 32 (its
first read), while the shared arm's guard stays at 28 so legal
header-only empty polylines keep loading. Undersized records are skipped
the way other malformed records already are. Tests: iType 7 and 91
records of exactly 28 bytes are skipped, a 36-byte record still renders.
- 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]>
A lone unlocked point cloud selected in a 3D visual style shows a move
gizmo at its insertion point: X/Y/Z arrows and XY/YZ/ZX squares. Dragging
an arrow moves the cloud along that axis, a square moves it in that
plane; the hovered part is highlighted and each drag is one undo step.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two causes behind the load stalling at the scene-cache step:
- Solids were fitted to the ACIS header's first tolerance-line value,
which is the millimetres-per-unit scale (25.4 in this inch drawing,
1 in most), not a tolerance. It is now resabs, the second value.
- The kernel (37b6902) drops pcurves that miss their edge and caps the pcurve
walk, so a solid carrying a bad pcurve no longer refines an edge
hundreds of thousands of times. Graph b741bd6.
The attached drawing now meshes all 76 solids in 3.4 s (release) and
opens in 19 s in a debug build.
Fixes#1538
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The manager's switches write the cloud's hidden scans (by the scan
identifier its project and scan file carry) and hidden regions (the
unassigned points are region 0), as the reference stores them, instead
of extended data. A parent switch shows mixed while some of its rows
are on. A cloud that shows none of its points draws its extents box.
The codec reads and writes both lists.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since 2026.39 reads `unifont 1.0` files (isocp.shx and its family among
them) instead of falling back to a substitute, a glyph built from a
subshape went wrong: code 7 is followed by a two-byte code point in a
unifont, but only big fonts read two bytes, so the low byte ran on as a
drawing opcode. ISOCP's `9` came out at x-height as a small bowl.
Fixes#1556
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>