#1574's dialog drew its pick, close, field and editor buttons with glyph
text (⌖ × ƒ …) that renders as tofu where the font lacks it; they use the
snap and close SVG icons and plain ASCII now. A tag set from Properties is
stored upper case like one from the dialogs, and the stray blank line the
PR left in Cargo.toml is gone.
Co-Authored-By: Claude Opus 5.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.
A Field dialog with the fields the application evaluates and the
reference format choices, FIELD placing a field as multi-line text, the
attribute definition dialogs' field buttons, field attachment on commit,
system variable / file size / regional date providers for field
evaluation, and the codec patch with the field authoring API.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
Align and Fit attributes run their text between the two baseline points,
an ATTDEF height becomes the next text height, and Multiple lines is not
carried to the next definition (measured).
Co-Authored-By: Claude Opus 5.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]>
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]>
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]>
Only DIMTSZ > 0 counted as ticks, so a style drawing its strokes with
the _OBLIQUE / _ARCHTICK arrow block gave each stroke a full arrow's
room: an 18-long dimension with 4.8 arrows moved its strokes outside
and ran the dim line past the extension lines once it was drawn live
(after any edit, #898). Tick arrow blocks now count as ticks for fit
and DIMDLE, like the file's own baked block shows them.
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:
SETVAR LUPREC and SETVAR AUPREC stored any parseable i16 verbatim, and
the header value flows through UnitContext into the per-call
formatters, which applied only a lower bound (.max(0)) with no upper
bound: format_length and format_angle rendered {:.*} with the raw
precision. SETVAR LUPREC 30000 therefore made format_length emit
30003-character strings on every properties and annotation formatting
call - a ~30 KB allocation per formatted value - while format_area in
the same file had long since capped itself with .min(15) and the units
dialog only ever offers 0-8 places. Negative values were also stored
verbatim.
Reproduce:
1. Command line: SETVAR LUPREC 30000 (or AUPREC 30000), then select
something so the properties palette formats lengths/angles.
2. Before: the header stores 30000 and each formatted value is ~30 KB
(the test measured 30003 chars for one length); -3 is stored as-is.
3. After: the setter stores 8 (0-8 range, echoed truthfully), -3
stores 0, 5 stores 5 - and even a file carrying LUPREC 30000 in its
header formats short output, because the formatters cap themselves.
Fix:
Two layers. The formatters get the cap format_area already uses:
format_signed_length and format_signed_angle now clamp with
.max(0).min(15), so every source - SETVAR, the units dialog, and
values carried in DWG/DXF files - produces bounded output on the hot
paths (15 matches the point past which f64 decimal output is
meaningless, which is why format_area was written that way). The
setters store x.clamp(0, 8) with the clamped value echoed back, the
same stay-inside-the-app's-own-UI-range rule SETVAR ISOLINES follows
against the Options slider. Architectural and fractional formats
ignore precision (fixed 1/64-inch resolution) and are unaffected.
Regression tests mirror the existing patterns: one forces hostile
precision through the formatters while saving and restoring the
thread-local unit context, one drives SETVAR through the command line
and checks the stored header values.
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:
evaluate_table_formula walked for row in r1.min(r2)..=r1.max(r2) across
columns with no bound, and table_cell_reference parses any usize -
=SUM(A1:A18446744073709551615) names about 1.8e19 rows. table.cell()
returns None outside the table, so results were correct; the loop just
never finished. The walk runs during tessellation, i.e. on the render
path - the app freezes permanently and cannot even be closed.
Reproduce:
1. Insert a table, double-click a cell.
2. Type =SUM(A1:A18446744073709551615) (or
=SUM(A1:A1000000000) - a minutes-long freeze) and press Enter.
3. Before: the next repaint hangs forever.
4. After: the value appears immediately.
Fix:
The walk is clamped to the table's own rows/columns: row_end =
r1.max(r2).min(rows - 1) (same for columns). Every skipped cell was a
None, so the collected values - and every function's result
(SUM/AVERAGE/COUNT/MIN/MAX) - are identical; the loop now runs at most
rows x columns. start > end after clamping is an empty range, so
out-of-table references simply collect nothing (as before). The
regression test also asserts total evaluation time under 5 seconds,
turning any future un-clamped loop into a fast failure.
Bug:
The properties panel's Rows/Columns fields feed apply_geom_prop's grow
loops: number.round() as usize with no upper bound - and parse_f64
accepts up to 1e15. while rows.len() < requested { add_row() } pushes
rows until the allocator aborts. The magnitude validation in this path
governs geometry, not iteration counts.
Reproduce:
1. Insert a table, select it, open Properties -> Rows (or Columns).
2. Enter 1000000000000000 and commit.
3. Before: grows until "memory allocation failed".
4. After: the table grows to at most 1,000,000 cells (e.g. 100,000 rows
x 10 columns) and stays responsive; a legitimate shrink to a smaller
number still works exactly.
Fix:
One product budget, MAX_TABLE_CELLS = 1_000_000, applied per dimension:
grow_to = requested.min(budget / other_dimension) clamps the grow loops
only - the shrink loops keep the raw requested count, so shrinks stay
exact and the two loops cannot fight (grow_to <= requested by
construction). A table loaded over the budget is preserved: growth is
skipped and only an explicit smaller request shrinks it. The product,
not per-dimension caps, is what bounds memory regardless of which field
is set first. Tests cover overshoot at cap+10 and the 1e15 input for
both fields.
Bug:
evaluate_table_formula located "(" with find (forward) and ")" with
rfind (backward), then sliced body[open+1..close]. For malformed input
like =)( the delimiters come out reversed and the slice panics ("slice
index starts at 2 but ends at 0") during tessellation - on the render
path, from a single pasted cell value.
Reproduce:
1. Insert a table and double-click a cell.
2. Type =)( (or =a)b(c ) and press Enter.
3. Before: the next repaint panics.
4. After: the formula does not evaluate; the literal text renders.
Fix:
body.get(open + 1..close)? - reversed delimiters yield None and the
caller's existing fallback shows the literal text. Well-formed formulas
are untouched: regression tests keep =SUM(A1:B2), =AVERAGE(A1:B2) and
=A1 evaluating, and reject =)(, =a)b(c and =).
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]>
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]>
POINTCLOUDCOLORMAP asks for a cloud (or None for the drawing's default
schemes) and opens the Point Cloud Color Map dialog: intensity and
elevation tabs with the scheme list, New / Delete / Rename, number of
colours (2-25, resampled along the scheme), blend between the ends,
reverse, gradient or bands, the colorized range, the out-of-range
choice and, for elevation, the cloud's extents or a fixed range with its
band height. OK / Apply write the drawing's colour map (created under
ACAD_POINTCLOUD_COLORMAP_DICT when missing) and the cloud's colouring in
one undo step; Make stylization current sets the tab's stylization.
Values outside a scheme's range take the end colours, the scan colours
or are hidden. Automation action pc_colormap edits the dialog.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A crop is drawn on the view plane: it keeps the view's right and up
directions in the cloud's coordinates and its points in 3D, and cuts
along the view. POINTCLOUDUNCROP removes a picked cloud's crops.
POINTCLOUDLOD scales the points shown in tenths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
POINTCLOUDATTACH opens the Attach Point Cloud dialog after the file is
chosen; its Attach button lives in the Attach Underlay list instead of a
separate Insert panel. A selected cloud brings the crop tools to the
right-edge toolbar. POINTCLOUDSTYLIZE sets the stylization with the
reference's prompts, the External References palette lists point clouds
(unload, reload, detach, path), and definitions keep the header extents.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR's principal-axis math now lives in the codec's transform_ellipse
(8464e0a), which explode, INSERT and every other codec path share.
transform_ellipse_geometry delegates to it for the block-cache tangent,
and the entity goes back through apply_standard_entity_transform, with
MIRROR delegated to the codec's apply_mirror: the old closure reflected
only the centre and major axis, leaving mirrored arcs on the wrong side,
and the PR's version turned the normal to -Z. The mirror test now checks
the kept +Z normal and the negated parameters.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Scans (.rcs) and projects (.rcp) are read and their points drawn as
screen-sized dots, coloured by the cloud's stylization. The cloud's
placement carries its scale and rotation; its extents and crops stay in
the scan's coordinates. A missing scan shows the extents with the saved
path. -POINTCLOUDATTACH / POINTCLOUDATTACH, POINTCLOUDCROP and the
POINTCLOUD* settings follow the reference's prompts and messages, the
Properties list is the reference's, and a locked cloud is left alone by
edits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every near-white colour turned black on a light background (and near-black
white on a dark one), including true colours. A 255,255,255 solid used as a
white mask became a black block. Now only colour 7 swaps: true colours and
ACI 255 resolve a hair off pure white/black and keep their value on screen
and in plots.
Discussion #1500
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Add transform_ellipse_geometry helper in scene::view::transform to compute exact principal semi-axes, normal, ratio, and shifted arc parameters under any affine transform (negative scales, reflections, negative rotations, non-uniform scaling, and translations).
- Use transform_ellipse_geometry in block_cache::transform_tangent for TangentGeom::PlanarEllipse so GPU analytical instances and curve endpoints match true WCS positions.
- Update entities::ellipse::apply_transform to use transform_ellipse_geometry for complete scaling, rotation, mirroring, and affine transformations.
- Clean up tangent parameters in to_render for Ellipse so start_param and end_param are consistently unwrapped.
- Add comprehensive unit tests in transform.rs, block_cache.rs, and ellipse.rs verifying negative rotations, negative scales, and distant origins.
It moved the text a fixed drawing unit off the line and then another unit
up, wrong at any real scale. It now sets DIMTAD = Above on the dimension
and lets the automatic layout place the text.
Fixes#543
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A fan only covers convex faces; architectural n-gons filled well outside
their outline (the sample's faces covered 63% extra area).
Fixes#680
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EXPLODE left MTEXT untouched. Each laid-out run becomes a TEXT at its own
baseline origin, height, rotation and width, keeping inline colours.
Fixes#760
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Middle (72 = 4) carries a Baseline vertical code, so the baseline landed on
the alignment point and the text sat half a height too high.
Fixes#767
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The synthesised dimension text copied the dimension's ByLayer colour, and
its glyphs take their colour from that entity, so the layer colour showed.
Fixes#898
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DWFATTACH / -DWFATTACH and DGNATTACH / -DGNATTACH attach a DWF / DWFx sheet
or a DGN (V7 or V8) model with the reference's prompts; ATTACH routes the
file types to them. The sheet or model is read into vector paths, drawn as a
transparent underlay raster (contrast, fade, monochrome and background
adjustment apply) and offers its geometry to object snaps. Definitions are
kept in ACAD_DWFDEFINITIONS / ACAD_DGNDEFINITIONS, the External References
palette lists and reloads them, and image textures get line-keeping mip
levels so thin underlay lines stay visible when zoomed out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The Frames list shows the drawing's frame state (hide / display and plot /
display but not plot / frames vary), follows commands, undo and document
switches, and lists the mixed state as a disabled item.
- DWFFRAME and DGNFRAME are frame variables of their own, kept in the header,
and FRAME sets them with the others.
- Out-of-range frame values say "Requires an integer between 0 and 2." and
ask again; a set value reports "Regenerating model.".
- IMAGEFRAME is stored in the image variables as bits (2 is written as 3) and
a drawing without them uses the profile value, which setting IMAGEFRAME
updates and the settings file keeps.
- Raster images, PDF underlays and wipeouts are picked on their frame only;
underlay snap geometry is never picked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings in: unchanged ACSH extrusion history and DIMASSOC records kept
byte for byte; native dynamic-action field layouts corrected; evaluation
graph expressions written as owned; evaluated block metadata copy;
source class numbers kept on same-version legacy saves; explicit block
attribute flags kept.
The dynamic-action model now names its fields after their native
meaning, so the object summaries read parameter ids, the move offsets'
distance multiplier and the lookup table's columns. The empty
PropertiesTable variant is gone; such tables stay opaque objects.
Pins opencadcodec 4f7bd51, opencadkernel 8186605, opencadgraph 048c9c5.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since the stored text point became authoritative (85619f49), new linear
and aligned dimensions drew their text at the placeholder the commands
stored (the middle of the dimension line), so DIMTAD Above/Below and the
text offset had no effect; a style or override edit left the stale point
in place too.
reset_automatic_text_position clears the point of text the user did not
place, so it is laid out from the current settings. DIMLINEAR, DIMALIGNED,
DIMBASELINE, DIMCONTINUE and QDIM call it on creation; a DIMSTYLE edit, a
dimension override row and a style change call it for the dimensions they
touch. Loaded, untouched dimensions keep their pictured position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every glyph advanced by the face's letter spacing, including CJK glyphs a
style's big font or the TrueType fallback supplies. Their advances already
carry the gap (big font) or side bearings (TrueType), so with an LFF primary
such as iso each ideograph took 1.33 heights instead of one.
Face::spacing_after(ch) gives the tracking only to glyphs the stroke font
draws itself; every layout path uses it.
The 1.8-height widths in the report were the TEXT bounds estimate counting
UTF-8 bytes; opencadcodec 1109d52 counts characters.
Pins opencadcodec 1109d52, opencadkernel 8a246b0, opencadgraph 3c4af1c.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- CLIP continues as IMAGECLIP on a raster image and as VPCLIP on a layout
viewport, with the reference prompts and stored data.
- Image clip boundaries sit on pixel centres, OFF only clears the display
flag, and an inverted boundary cuts the image.
- VPCLIP polygon (Arc/Length/Close/Undo), clipping object and Delete; the
viewport shrinks to the boundary's exact extents, view kept.
- XCLIP Clipdepth: Remove clears only the plane asked, a front plane behind
the back one is rejected with the reference message.
- Attached and imported images carry the reference display flags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.
A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The command driver was split into command_driver/ upstream; the PDF
underlay and XCLIP additions to the old file move with it: their
CmdResult arms into the router (mod.rs) and the block-reference
selection filter with its clip lookup into utilities.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Publishes app-free domain logic for shared use: match_special_props and PolylineMatchStyle move to entities::match_props; xdict trio usable headlessly; new match_layer, copy_to_clipboard, paste_entities and match_properties kernels over CadDocument. Handlers stay thin shells keeping undo, dirty flags, selection and command-line echo.
Adds cross-document copy, paste and property-match tests plus a headless proof driving copy, paste and match with no app instance.