Skip to content

editor: let the clipboard validate plugin-contributed node kinds#547

Open
ovurrsl wants to merge 1 commit into
pascalorg:mainfrom
ovurrsl:fix/clipboard-plugin-nodes
Open

editor: let the clipboard validate plugin-contributed node kinds#547
ovurrsl wants to merge 1 commit into
pascalorg:mainfrom
ovurrsl:fix/clipboard-plugin-nodes

Conversation

@ovurrsl

@ovurrsl ovurrsl commented Jul 25, 2026

Copy link
Copy Markdown

What does this PR do?

scene-clipboard.ts validates every node it copies or pastes with AnyNode.parse. AnyNode is the hand-maintained discriminated union of built-in kinds, so a kind contributed through registerNode can never be a member of it — the registry validates those against def.schema at runtime instead.

The effect is that capabilities.duplicable cannot be honoured by any plugin. Copying a plugin node throws invalid_union / no matching discriminator; pasting drops it silently, because parseClipboardPayload treats a failed parse as an invalid payload and returns null. This includes the first-party pascal:trees pack, which declares duplicable: true and fails identically.

Nothing in the plugin API works around it from the outside: DuplicableConfig exposes only subtree and prepareSubtreeClone, and both run after the parse that throws.

This adds parseClipboardNode, which tries AnyNode first and falls back to the registry's schema for the node's type. It is applied at both call sites — the remapNodeReferences return, and the system-clipboard parse in parseClipboardPayload.

Built-in behaviour is unchanged: AnyNode is still attempted first, and a type that is neither built-in nor registered still raises the original AnyNode error rather than a vaguer one.

Found while building a third-party node pack against plugin API v1; the same failure reproduces with pascal:trees alone.

How to test

  1. cd packages/editor && bun test src/lib/scene-clipboard.test.ts — 7 pass.
  2. Revert just scene-clipboard.ts (git checkout main -- packages/editor/src/lib/scene-clipboard.ts) and re-run: the new test fails with Invalid discriminator value. Expected 'site' | 'building' | ..., which is the error the editor surfaces today.
  3. In the app, with any plugin that declares capabilities.duplicable installed (e.g. pascal:trees): place a plugin node, select it, Ctrl/Cmd+C then Ctrl/Cmd+V. Before this change nothing is pasted and the console shows the union error; after it, the node duplicates like a built-in.
  4. Regression check on built-ins: copy/paste a cabinet run, a wall + measurement pair, and a standalone window — all still behave as before (covered by the pre-existing tests in the same file).

Screenshots / screen recording

N/A — non-visual change. The observable difference is a paste that previously did nothing now producing a node.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

Note for the reviewer: packages/editor currently has one pre-existing unrelated failure on mainfloorplan-export.test.ts > resolveSheetComposition > reports duplicate reusable and sheet-local general notes. It fails identically without this branch checked out.


Note

Low Risk
Localized change to clipboard parsing with built-in-first behavior preserved; risk is mainly accepting malformed plugin payloads only when a registry schema exists.

Overview
Clipboard copy/paste no longer rejects plugin node kinds because validation went through AnyNode only, which cannot include kinds registered via registerNode.

The editor adds parseClipboardNode, which still parses built-ins with AnyNode and otherwise validates against nodeRegistry.get(type).schema. That helper replaces direct AnyNode.parse when remapping nodes on paste and when parsing nodes from the system clipboard payload, so capabilities.duplicable can work for plugin packs (e.g. trees). Unknown or unregistered types still fail with the original AnyNode error.

Tests register a fake plugin kind, assert copy/paste preserves kind-specific fields, and nodeRegistry._reset() in afterEach so registry state does not leak between tests.

Reviewed by Cursor Bugbot for commit e8e2e58. Bugbot is set up for automated code reviews on this repo. Configure here.

`scene-clipboard.ts` validates every node it copies or pastes with
`AnyNode.parse`. `AnyNode` is the hand-maintained discriminated union of
built-in kinds, so a kind contributed through `registerNode` can never be
a member of it — the registry validates those against `def.schema` at
runtime instead.

The effect is that `capabilities.duplicable` cannot be honoured by any
plugin. Copying a plugin node throws `invalid_union / no matching
discriminator`; pasting drops it silently, because
`parseClipboardPayload` treats a failed parse as an invalid payload and
returns null. This includes the first-party `pascal:trees` pack, which
declares `duplicable: true` and fails identically.

Nothing in the plugin API works around it from the outside:
`DuplicableConfig` exposes only `subtree` and `prepareSubtreeClone`, and
both run after the parse that throws.

Add `parseClipboardNode`, which tries `AnyNode` first and falls back to
the registry's schema for the node's `type`. Applied at both call sites —
the `remapNodeReferences` return, and the system-clipboard parse in
`parseClipboardPayload`.

Built-in behaviour is unchanged: `AnyNode` is still attempted first, and
a type that is neither built-in nor registered still raises the original
`AnyNode` error rather than a vaguer one.

The test registers a definition exactly as a plugin does at load time,
then copies and pastes it. Without the fallback it fails with the same
`Invalid discriminator value` the editor reports.
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