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]>
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]>
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:
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.
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]>
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]>
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]>
A dockable Point Cloud Manager lists each point cloud of the drawing (only
the selected ones while any is selected) with Regions (disabled, region
data is not read), Unassigned Points and Scans, the scans named by their
file stems. Each switch shows or hides points: the cloud, its unassigned
points, all scans or one scan. The hidden set is the cloud's
OCS_POINTCLOUD_VISIBILITY extended data (scan names plus *CLOUD_OFF /
*UNASSIGNED_OFF markers, app id registered), written as one undo step and
kept on save. The drawn points skip hidden scans by their index ranges,
and the hidden set is part of the placed-cloud key. Collapse all / expand
all, row highlight and a search box that filters rows (keeping parents of
matches). POINTCLOUDMANAGERCLOSE closes it; the pc_manager_toggle
automation action flips a switch.
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]>
Scan normals are decoded from their cube-map codes and drawn as world
normal colours under the Normals stylization; project and scan rotation
angles are read as degrees.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A scheme lists its colours from the highest value down (Spectrum:
magenta at the highest intensity, red at the lowest), as the reference
colours an intensity- or elevation-stylized cloud.
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 byte count at 110 was read with direct indexing; a truncated element
now skips like every other field read does.
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]>
A DWF sheet or DGN model is drawn as world-space wires (one line wire per
colour and width, one fill wire per colour) instead of a raster rescaled
to the view, so it stays sharp while zooming instead of showing pixels
until the raster is made again. Colours go through the colour table
mapping, contrast, fade and monochrome; clip boundaries clip the wires.
The wires are drawn and plotted but neither picked nor snapped; the
underlay's own snap geometry still serves object snaps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One-pixel hairlines keep horizontal and vertical pieces solid, but
slanted pieces and curves are anti-aliased: their pixel steps broke up
into dashes once the raster was scaled to the screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- V8 models are placed relative to their global origin (model header).
- A V8 model's extent is the union of its elements' stored ranges; a V7
model's text box is one text height tall.
- Files without a colour table use the standard DGN default table; colour
255 is read from the table's background entry.
- Colours from a file's colour table follow the background: near-white
colours swap with it, and over non-black backgrounds some shades drop a
step.
- DGN contrast pulls towards a pivot taken from the model's own colours,
after the fade, which is blended into the background first.
- Shared cells are read through their full 3D placement, so tilted
instances and 3D definitions are drawn in plan.
- Deleted V8 elements are skipped; lines along the model's extent stay in
the raster.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The DWF package reader, the DGN stream inflater and the new DGN
ExtendedColors scan each read a zlib/zip payload to its end, so a
crafted or corrupt file could inflate until memory ran out. Cap them:
1 GiB for a whole DWF package, 512 MiB per DGN stream, 4 MiB per
ExtendedColors record candidate - far above real files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Insert > Reference: the PDF Attach button becomes an Attach Underlay menu
(PDF / DWF / DGN); its face runs the last one chosen.
- DWFATTACH, DGNATTACH and ATTACH on a .dwf/.dwfx/.dgn open the Attach
dialog for that kind: sheets or models with thumbnails (one chosen),
placement, path type, file details with the sheet / model size, and for
DGN the conversion units (sub units offer 1 / sub units per master as the
scale).
- DGN V8 true colours come from the ExtendedColors record packed in the
non-model attributes stream; the colour word's high part numbers them.
- A DGN model's extent includes each text's box (its length along the
baseline and 1.5 x its height), as the reference sizes a model.
- DWFx text uses the family named in its embedded (deobfuscated) font.
- DWF contrast pivots at 0.73 once colours are turned over for a dark
background.
- Hairline strokes are drawn solid one pixel wide, and underlay rasters are
made at or below their size on screen so those lines stay unbroken.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- W2D: a stream cut off right after an extended opcode's '(' sliced its
body backwards and panicked.
- DGN V8: the compound-file header was indexed before its length was
known (a short file with the signature panicked), the sector shift was
taken on trust (0 or 1 underflowed `sector / 4 - 1` into an endless
loop), and the DIFAT chain count was unbounded. The header must now be
complete, the shift 9 or 12, and the chain no longer than the file.
Found fuzzing the #1514 readers with truncated and random input.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- PDFOSNAP, DWFOSNAP and DGNOSNAP are separate; UOSNAP sets them all and
reads 2 while they differ. Enable Snap switches the selected kind's.
- DWFCLIP / DGNCLIP clip their own kind with the kind's prompts and message;
CLIP routes DWF and DGN underlays there; Enter at a corner asks again.
- The right-edge underlay tools show for DWF and DGN too (import is PDF
only).
- Underlay layers: DGN levels (V8 level table, "Default" listed as "0"; V7
used levels named from the level name table), hidden through the layer
override data with the DGN placeholder string first; a plotted DWF has
none.
- V8 colour read from its colour word (by level, index, or a true colour
drawn as its nearest index); 3D V8 elements drawn in plan (lines, line
strings, ellipses and arcs by their orientation, text); a model's extent
includes its text.
- DWF 6 TrueType text (font, height, rotation from the font opcode) and DWFx
glyph runs are drawn; the default W2D colour map is the measured one.
- Contrast pulls lightness towards a per-kind pivot as measured (PDF 0.5,
DWF 0.243, DGN 0.65).
- Zoom extents includes raster images and underlays whose frames are hidden.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Frames: follow the drawing state, DWF/DGN frames, image frame profile, frame-only picking
The codec [patch] to ramox81/cadcodec@ea5edaa is dropped: that commit landed as opencadcodec#63 and main already pins ad16215, which contains it.
A page shown small was drawn from its full-size raster and thin lines broke
up into dots. Once a zoom settles on another quarter-octave step, PDF, DWF
and DGN underlays are rasterised again near their size on screen (never
above the full-size raster), frames are requested until that happens, and
the adjusted-pixel memo is kept per raster size. V7 element weights no longer
widen strokes, matching V8 models.
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]>
An image's transparency flag decides whether transparent pixels show what
is behind the image or draw in their stored colour: the image pass and PDF
plots now follow it (it was ignored, so every PNG alpha showed through).
TRANSPARENCY selects images and asks for the mode, defaulting to the first
image's current one.
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]>
Review of #1484:
- The underlay's snap geometry became one hidden wire per segment, but
every piece took the whole page's bounding box, so each cursor move
visited every piece. Each piece now gets its own box.
- Assigning unlayered images to the last layer compared Debug strings of
every layered path and text per drawn item (quadratic, formatted anew
each time); the owners are keyed once in a map.
- The `snap` automation op used (0, 0, 0) for a missing point; it now
answers invalid_point. It also left the live snapper's base point set
to the query's; the base point is restored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Raster image textures use nearest magnification; PDF pages and OLE
pictures keep bilinear filtering.
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]>
- Unlayered raster images go with the PDF layer drawn last before them.
- Circular Bezier arcs import as arcs: bulged polyline segments with Join,
ARC entities without it; straight chains still join.
- Imported PNGs go to "PDF Images" next to the PDF; import blocks are
numbered imp31, imp32, ...
- Background darkness is its HSL lightness below one half.
- All chosen pages attach at the same insertion point.
- PDF geometry snaps as separate pieces: centre from the rim, intersections
and perpendiculars.
- Automation: snap query with an optional base point.
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]>