Skip to content

Reconcile the Two.js scene graph with React updates - #36

Merged
jonobr1 merged 5 commits into
mainfrom
fix_issue_29_react
Sep 3, 2026
Merged

Reconcile the Two.js scene graph with React updates#36
jonobr1 merged 5 commits into
mainfrom
fix_issue_29_react

Conversation

@jonobr1

@jonobr1 jonobr1 commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Walkthrough: React Two.js Scene Graph Reconciliation (Issue #29)

We resolved GitHub Issue #29 on branch fix_issue_29_react, addressing all 9 acceptance criteria for React-controlled Two.js scene-graph reconciliation, prop resets, discrete prop diffing, in-place construction prop updates, and Strict Mode lifecycle safety.


1. Summary of Changes

A. Centralized Reconciliation Core

  • reconciliation.ts:
    • Defined TWO_DEFAULT_PROPS capturing standard Two.js defaults (fill: '#fff', stroke: '#000', linewidth: 1, opacity: 1, visible: true, x: 0, y: 0, etc.).
    • Implemented captureDefaultProps(instance) to snapshot initial properties of any instantiated Two.js shape.
    • Implemented diffProps(prev, next, ignored) for discrete prop diffing, partitioning properties into changed and removed.
    • Implemented reconcileSceneOrder(parent, targetOrder) to sort parent.children in-place and set parent._flagOrder = true.

B. Shared Lifecycle Hooks

  • useTwoObject.ts:
    • Created useTwoObject hook:
      • Synchronous factory & construction props: recreates instance when construction-only props (e.g. resolution, sides, src) change, performing in-place replacement at the identical index in parent.children.
      • Discrete prop diffing: updates changed props without touching unrelated props or dirtying unaffected Two.js flags.
      • Default resets: restores captured Two.js defaults when props are removed or set to undefined.
      • Scene-graph attachment & reparenting: properly coordinates parent.children attachment and detachment when parent changes.
      • JSX document-order sibling registration: registers child order via registerChildOrder during commit layout effect.
      • Idempotent event registration: cleans up event handlers on unmount and updates them cleanly.
    • Created useTwoGroup hook for container components (Group, SVG, Provider):
      • Collects child ordering during layout effects and invokes reconcileSceneOrder(instance, childrenOrderRef.current).
      • Coordinates child slots via ChildSlotContext.Provider so memoized children (React.memo) detect sibling reorders.

C. Context & Provider Architecture

  • Context.ts:
    • Extended TwoParentContextValue with attachChild, detachChild, registerChildOrder.
    • Added ChildSlotContext for sibling position notification across memo boundaries.
  • Provider.tsx:
    • Implemented attachChild, detachChild, registerChildOrder for the root two.scene.
    • Added reconcileSceneOrder during root layout effect.
    • Wrapped root children in ChildSlotContext.Provider.
    • Hardened Strict Mode unmount cleanup to clear event shapes and hover tracking.

D. Wrapper Modernization (Deleted >1,400 lines of duplication)

Refactored all wrappers to use useTwoObject and useTwoGroup:

  • Group.tsx
  • Circle.tsx (constructionProps: ['resolution'])
  • Rectangle.tsx
  • RoundedRectangle.tsx
  • Ellipse.tsx (constructionProps: ['resolution'])
  • Line.tsx (specialProps: ['x1', 'y1', 'x2', 'y2'])
  • Path.tsx (specialProps: ['manual'])
  • Points.tsx
  • Polygon.tsx (constructionProps: ['sides'])
  • Star.tsx
  • ArcSegment.tsx (constructionProps: ['resolution'])
  • Text.tsx
  • Image.tsx (constructionProps: ['src'])
  • Sprite.tsx (constructionProps: ['src'])
  • ImageSequence.tsx (constructionProps: ['src'])
  • LinearGradient.tsx (isSceneObject: false)
  • RadialGradient.tsx (isSceneObject: false)
  • Texture.tsx (isSceneObject: false)
  • SVG.tsx

2. Verification & Acceptance Criteria

We created tests/reconciliation.test.tsx to explicitly verify all criteria:

Acceptance Criterion Test Verification Status
1. Reordering keyed JSX siblings produces matching Group.children order reorders parent.children when keyed JSX siblings reorder PASS
2. Reordering works with React.memo components reorders parent.children even when sibling components are wrapped in React.memo PASS
3. Moving keyed object between groups moves a keyed object between groups, removing from old parent and inserting into new parent & reparents the same component instance when parent context changes dynamically PASS
4. Discrete prop updates without reassigning unrelated props updates only changed properties without reassigning unchanged properties (verified _flagStroke and _flagFill remain untriggered) PASS
5. Removed props reset to Two.js defaults restores Two.js default values when props are removed or set to undefined & restores default values on Text component when props are removed PASS
6. Construction-only prop replacement in-place recreates instance safely in-place at the exact child index when construction props change PASS
7. Strict Mode double-mount safety handles StrictMode double-mount without leaking duplicate children or event handlers PASS
8. Distinguish owned vs shared resources distinguishes owned resources from shared resources on unmount PASS
9. Comprehensive test suite 17 test files, 157 passing tests PASS

Test Suite Execution

 RUN  v3.2.4 /Users/jonobrandel/.gemini/antigravity/worktrees/react-two.js/fix_issue_29_react

 ✓ tests/wiremarksStorage.test.ts (11 tests)
 ✓ tests/diffsStorage.test.ts (7 tests)
 ✓ tests/wiremarks.test.ts (17 tests)
 ✓ tests/diffsModel.test.ts (6 tests)
 ✓ tests/diffsReveal.test.ts (2 tests)
 ✓ tests/wiremarksGraph.test.ts (12 tests)
 ✓ tests/wiremarkConnection.test.tsx (3 tests)
 ✓ tests/events.test.tsx (12 tests)
 ✓ tests/wiremarkEntity.test.tsx (3 tests)
 ✓ tests/reconciliation.test.tsx (12 tests)
 ✓ tests/zui.test.tsx (16 tests)
 ✓ tests/diffsStatLine.test.tsx (5 tests)
 ✓ tests/zuiMath.test.ts (14 tests)
 ✓ tests/wiremarksLayout.test.ts (26 tests)
 ✓ tests/diffsTokenize.test.ts (4 tests)
 ✓ tests/diffsCanvas.test.tsx (5 tests)
 ✓ tests/registry.test.ts (2 tests)

 Test Files  17 passed (17)
      Tests  157 passed (157)

Build & Lint

  • eslint .: 0 errors, 0 warnings.
  • npm run build: built production bundles and type declarations cleanly.

Copilot AI lite review requested due to automatic review settings September 3, 2026 00:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

useTwoObject performs parent/registry mutations during render for recreation, which can violate React render guarantees and risk corrupting scene graph state.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR introduces a centralized reconciliation layer so React updates (reorder, reparent, prop diff/removal, construction-prop replacement, and Strict Mode behavior) keep the underlying Two.js scene graph consistent after the initial mount.

Changes:

  • Added a shared reconciliation core (diffProps, default capture/reset, and in-place scene order reconciliation).
  • Introduced useTwoObject / useTwoGroup hooks and refactored wrappers to use them.
  • Added a dedicated test suite validating Issue #29’s reconciliation semantics.
File summaries
File Description
tests/reconciliation.test.tsx Adds coverage for ordering, reparenting, discrete prop diffing/resets, construction-prop replacement, and Strict Mode lifecycle behavior.
lib/useTwoObject.ts Implements shared object lifecycle (create/recreate, prop diff/apply/reset, attachment/reparenting, child order registration, event registration).
lib/reconciliation.ts Defines default prop semantics, captures per-instance defaults, computes discrete diffs, and reconciles children order in-place.
lib/Context.ts Extends parent context with child attach/detach and sibling order registration; adds ChildSlotContext for memo-safe reorders.
lib/Provider.tsx Implements root-level attach/detach/order registration and reconciles root scene order; wires ChildSlotContext providers.
lib/main.ts Exports reconciliation internals (useTwoObject, useTwoGroup, TWO_DEFAULT_PROPS).
lib/Group.tsx Refactors Group wrapper to use useTwoGroup and slot-based child rendering.
lib/SVG.tsx Refactors SVG wrapper to use useTwoGroup; updates async load flow and child rendering.
lib/Circle.tsx Refactors Circle wrapper to useTwoObject (construction prop: resolution).
lib/Rectangle.tsx Refactors Rectangle wrapper to useTwoObject.
lib/RoundedRectangle.tsx Refactors RoundedRectangle wrapper to useTwoObject.
lib/Ellipse.tsx Refactors Ellipse wrapper to useTwoObject (construction prop: resolution).
lib/Line.tsx Refactors Line wrapper to useTwoObject with special endpoint prop handling.
lib/Path.tsx Refactors Path wrapper to useTwoObject with special manual handling.
lib/Points.tsx Refactors Points wrapper to useTwoObject.
lib/Polygon.tsx Refactors Polygon wrapper to useTwoObject (construction prop: sides).
lib/Star.tsx Refactors Star wrapper to useTwoObject.
lib/ArcSegment.tsx Refactors ArcSegment wrapper to useTwoObject (construction prop: resolution).
lib/Text.tsx Refactors Text wrapper to useTwoObject.
lib/Image.tsx Refactors Image wrapper to useTwoObject (construction prop: src; special mode/texture).
lib/Sprite.tsx Refactors Sprite wrapper to useTwoObject (construction prop: src; special autoPlay; pause on dispose).
lib/ImageSequence.tsx Refactors ImageSequence wrapper to useTwoObject (construction prop: src; special autoPlay; pause on dispose).
lib/LinearGradient.tsx Refactors LinearGradient wrapper to useTwoObject (non-scene object; special endpoint props).
lib/RadialGradient.tsx Refactors RadialGradient wrapper to useTwoObject (non-scene object; special center/focal props).
lib/Texture.tsx Refactors Texture wrapper to useTwoObject (non-scene object; construction prop: src).
Review details
  • Files reviewed: 25/25 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/useTwoObject.ts Outdated
Comment thread lib/useTwoObject.ts Outdated
Comment thread lib/SVG.tsx Outdated
Comment thread lib/Texture.tsx
…llow SVG branch, and broaden Texture src type

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A confirmed edge case in useTwoObject can leak/duplicate children when construction-prop recreation coincides with a parent change.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 25/25 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread lib/SVG.tsx Outdated
Comment thread lib/useTwoObject.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Prop-removal reset semantics are incomplete for some publicly supported props (e.g. position/skewX/skewY/matrix/worldMatrix), which can leave stale values on instances after props are removed.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

lib/reconciliation.ts:29

  • useTwoObject resets removed props using defaultPropsRef / TWO_DEFAULT_PROPS, but captureDefaultProps() only captures keys listed in TWO_DEFAULT_PROPS. Several props that are publicly exposed via ShapeProps (e.g. position, skewX, skewY, matrix, worldMatrix in lib/Properties.ts) are missing here, so removing those props will leave stale values on the Two.js instance (no reset occurs because defaultVal stays undefined). Adding these keys to TWO_DEFAULT_PROPS (even with undefined placeholders for non-primitives) allows captureDefaultProps() to snapshot them and makes prop-removal resets work consistently.
  • Files reviewed: 25/25 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

# Conflicts:
#	lib/Group.tsx
#	lib/Image.tsx
#	lib/ImageSequence.tsx
#	lib/Path.tsx
#	lib/Points.tsx
#	lib/Rectangle.tsx
#	lib/SVG.tsx
#	lib/Sprite.tsx
#	lib/Text.tsx
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.

[Parity] Reconcile the Two.js scene graph with React updates

2 participants