Skip to content

polyhedron(vnf), and object()'s single-element delete entry - #106

Merged
revarbat merged 2 commits into
mainfrom
polyhedron-accepts-vnf
Aug 21, 2026
Merged

polyhedron(vnf), and object()'s single-element delete entry#106
revarbat merged 2 commits into
mainfrom
polyhedron-accepts-vnf

Conversation

@revarbat

Copy link
Copy Markdown
Member

Two independent fixes, both reported while using render() expressions in anger.

object(): a single-element entry deletes that key

a = object(a=42, b=53, c=8);
b = object(a, [["d", 18], ["b"]]);   // -> { a = 42; c = 8; d = 18; }

An unnamed list argument holds entries: [key, value] sets, and the single-element [key] deletes. Only the two-element form was implemented, so any script using a delete got undef for the whole call — silently, with no diagnostic pointing at why.

Checking the rest of the semantics against OpenSCAD 2026.02.01 turned up more than the reported gap:

before now
[["b"]] delete undef deletes
[["zz"]] delete a missing key undef silent no-op
malformed entries undef, silent undef + warning

Deletion removes the key rather than blanking it, which is observable because ValueObject is insertion-ordered and oscEqual is order-sensitive: object(a, [["b"], ["b", 99]]) puts b last, while object(a, [["b", 99], ["b"]]) has no b at all. Both are pinned.

All 19 cases — values and warning text — were diffed character-for-character against the reference binary with --enable=object-function. The diff is empty. The warnings reproduce two of upstream's own inconsistencies: the "not a list" case puts spaces inside its parens where every other case does not, and the "unnamed argument" case ends with a trailing space. Those strings are what a user greps for, so matching them is the point.

mergeObjectArgs gained an Evaluator and a position so it can warn; both call sites — the interpreter's builtinObject and the VM's Op::CallFn — pass them through.

polyhedron() accepts a VNF 2-list

[vertices, faces] is what every BOSL2 function passes around and what obj.vnf holds. All four of these are now the same call:

polyhedron(obj);
polyhedron(obj.vnf);
polyhedron(obj.vertices, obj.faces);
polyhedron(spheroid(d=30));          // any BOSL2 VNF

Only considered when faces was not supplied separately, so the two-argument form always wins and can never be reinterpreted. The discriminator is BOSL2's own is_vnf test: a VNF's second element is a list of lists, where a points list that happens to have length 2 has a point there — bare numbers. Such a list is left alone and still errors as the invalid polyhedron it is.

Verified both directions against real BOSL2: polyhedron(spheroid(...)) matches the volume vnf_polyhedron() produces, and vnf_polyhedron(obj.vnf) accepts ours.

Testing

967 tests pass under both OSCAD_BYTECODE_VM=0 and =1.

🤖 Generated with Claude Code

revarbat and others added 2 commits August 20, 2026 21:54
[vertices, faces] is what every BOSL2 function passes around and what
obj.vnf holds, so it should go straight in rather than having to be split
apart at the call site. All four of these are now the same call:

    polyhedron(obj);
    polyhedron(obj.vnf);
    polyhedron(obj.vertices, obj.faces);
    polyhedron(spheroid(d=30));          // any BOSL2 VNF

Only considered when `faces` was not supplied separately, so the
two-argument form always wins and can never be reinterpreted.

The discriminator is BOSL2's own is_vnf test: a VNF's second element is a
list of FACES, i.e. a list of lists. A points list that happens to have
length 2 has a POINT there instead -- bare numbers, not lists -- so it is
left alone and still errors as the invalid polyhedron it is. Two points
cannot describe a polyhedron anyway, so nothing legitimate is taken over.

Verified both directions against real BOSL2: polyhedron(spheroid(...))
matches the volume vnf_polyhedron() produces, and vnf_polyhedron(obj.vnf)
accepts ours.

Minor, not patch: new accepted argument shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    a = object(a=42, b=53, c=8);
    b = object(a, [["d", 18], ["b"]]);   // -> { a = 42; c = 8; d = 18; }

An unnamed list argument holds entries, and [key, value] SETS while the
single-element [key] DELETES. Only the two-element form was implemented, so
any script using a delete got undef for the whole call -- silently, with no
diagnostic pointing at why.

Checking the rest of the semantics against OpenSCAD 2026.02.01 turned up
more than the reported gap:

  - deleting an absent key must be a silent no-op (was: undef)
  - every malformed entry must WARN as well as yield undef (was: silent)

Deletion removes the key outright rather than blanking it, which is
observable because ValueObject is insertion-ordered and oscEqual is
order-sensitive: object(a, [["b"], ["b", 99]]) puts b LAST, while
object(a, [["b", 99], ["b"]]) has no b at all. Both are pinned.

The warning text is quoted verbatim from the reference, including two of
its own inconsistencies -- the "not a list" case puts spaces inside its
parens where every other case does not, and the "unnamed argument" case
ends with a trailing space. Reproducing them is the point: these strings
are what a user greps for.

All 19 cases (values AND warnings) were diffed character-for-character
against the reference binary with --enable=object-function; the diff is
empty. mergeObjectArgs gained an Evaluator and a position so it can warn,
which both call sites -- the interpreter's builtinObject and the VM's
Op::CallFn -- now pass through.

967 tests pass under both engines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@revarbat
revarbat merged commit 09d2bb1 into main Aug 21, 2026
3 checks passed
@revarbat
revarbat deleted the polyhedron-accepts-vnf branch August 21, 2026 10:47
revarbat added a commit to BelfrySCAD/BelfrySCAD that referenced this pull request Aug 21, 2026
Brings polyhedron(vnf) and object()'s single-element delete entry
(BelfrySCAD/openscad_cpp_evaluator#106), both of which this release's
viewers assume.

Locked only AFTER PyPI's index listed all 7 files, rather than the moment
the wheels workflow reported success. Locking on workflow-success alone
produced a 3-of-7 lock last release -- missing Windows, manylinux x86_64
and the sdist -- which nothing but the post-lock file count catches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
revarbat added a commit to BelfrySCAD/BelfrySCAD that referenced this pull request Aug 21, 2026
* Viewers for object() and for geometry objects

Three related gaps, all reached from the same right-click menus.

**Geometry objects reach the mesh viewers.** A render() expression yields
an object() carrying the mesh as separate `vertices`/`faces` keys, so
_is_vnf -- which only ever matches a bare 2-list -- fired for obj.vnf but
never for obj itself. _geometry_object_vnf unwraps it. The 2D case is not a
straight unwrap: `paths` holds INDICES into `vertices`, so the contours are
resolved to real points first, which makes a region (a list of closed 2D
paths) -- what a 2D shape with holes actually is. Dispatch is on `dim`:
3 -> VNF viewer, 2 -> Region viewer, plus Path when there is exactly one
contour, following the existing "offer both readings and let the user pick"
convention.

Deliberately NOT folded into _is_vnf: that predicate also spots VNF
literals in source text, where an object is not a candidate at all.

**ObjectViewer.** Key/Type/Value, read-only by default -- the only
possibility for a debugger value, which has no source span to write back
to -- and editable from "Edit as Object..." with add/delete/rename, undo,
and a Save that rewrites the call. Values are shown and typed as OpenSCAD
source (`true`, `undef`, `[1,2]`), since that is what is being edited.

An object is a CALL, not a bracket literal, so it needed its own span
finder and text round-trip rather than riding on
_iter_enclosing_literals/ast.literal_eval.

**Editing is offered only for all-literal calls.** `object(other, [["b"]])`
depends on `other`, whose contents are unknowable from source text, so
rewriting it would silently destroy the reference. _parse_object_call_args
returns None for those and they stay view-only.

Two bugs found while building this, both pinned by tests:

- Translating OpenSCAD's true/false/undef to Python cannot be a str.replace:
  it turns the string "undefined" into "Noneined". The translation skips
  quoted regions.
- A duplicate key overwrites IN PLACE and keeps its original position --
  object(a=42, b=1, a=99) is { a = 99; b = 1; }, checked against the
  reference binary. Remove-and-append would have been DELETE semantics, and
  the difference is observable because an object is insertion-ordered.

And one the user caught: "View as Object" on a call referencing a variable
opened an EMPTY viewer, because the lexical menu offered a source-text view
it could not populate. That view is now gated on the call being resolvable,
with a second guard in _open_object_viewer so an empty window cannot appear
by another route. Such objects remain fully inspectable at runtime, where
the debugger hands over resolved values.

834 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Require openscad_cpp_evaluator 0.39.0; bump to 0.76.0

Brings polyhedron(vnf) and object()'s single-element delete entry
(BelfrySCAD/openscad_cpp_evaluator#106), both of which this release's
viewers assume.

Locked only AFTER PyPI's index listed all 7 files, rather than the moment
the wheels workflow reported success. Locking on workflow-success alone
produced a 3-of-7 lock last release -- missing Windows, manylinux x86_64
and the sdist -- which nothing but the post-lock file count catches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant