marimo-cad 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. marimo_cad-0.1.0/.github/workflows/publish.yml +56 -0
  2. marimo_cad-0.1.0/.gitignore +57 -0
  3. marimo_cad-0.1.0/LICENSE +21 -0
  4. marimo_cad-0.1.0/PKG-INFO +179 -0
  5. marimo_cad-0.1.0/README.md +146 -0
  6. marimo_cad-0.1.0/assets/demo.gif +0 -0
  7. marimo_cad-0.1.0/js/ARCHITECTURE.md +214 -0
  8. marimo_cad-0.1.0/js/package-lock.json +1506 -0
  9. marimo_cad-0.1.0/js/package.json +19 -0
  10. marimo_cad-0.1.0/js/src/cad-group-factory.js +196 -0
  11. marimo_cad-0.1.0/js/src/clipping-extension.js +206 -0
  12. marimo_cad-0.1.0/js/src/constants.js +95 -0
  13. marimo_cad-0.1.0/js/src/index.js +165 -0
  14. marimo_cad-0.1.0/js/src/live-viewer.js +339 -0
  15. marimo_cad-0.1.0/js/src/part-manager.js +365 -0
  16. marimo_cad-0.1.0/js/src/state-manager.js +164 -0
  17. marimo_cad-0.1.0/js/src/styles.css +18 -0
  18. marimo_cad-0.1.0/js/vite.config.js +26 -0
  19. marimo_cad-0.1.0/notebooks/bookshelf.py +104 -0
  20. marimo_cad-0.1.0/notebooks/sample_part.step +511 -0
  21. marimo_cad-0.1.0/notebooks/vase.py +224 -0
  22. marimo_cad-0.1.0/package-lock.json +79 -0
  23. marimo_cad-0.1.0/package.json +31 -0
  24. marimo_cad-0.1.0/playwright.config.js +36 -0
  25. marimo_cad-0.1.0/pyproject.toml +57 -0
  26. marimo_cad-0.1.0/scripts/record-demo.js +81 -0
  27. marimo_cad-0.1.0/src/marimo_cad/__init__.py +20 -0
  28. marimo_cad-0.1.0/src/marimo_cad/constants.py +68 -0
  29. marimo_cad-0.1.0/src/marimo_cad/export.py +107 -0
  30. marimo_cad-0.1.0/src/marimo_cad/static/widget.css +1 -0
  31. marimo_cad-0.1.0/src/marimo_cad/static/widget.js +50871 -0
  32. marimo_cad-0.1.0/src/marimo_cad/tessellate.py +311 -0
  33. marimo_cad-0.1.0/src/marimo_cad/utils.py +78 -0
  34. marimo_cad-0.1.0/src/marimo_cad/widget.py +175 -0
  35. marimo_cad-0.1.0/tests/__init__.py +1 -0
  36. marimo_cad-0.1.0/tests/e2e/cad-viewer.spec.js +288 -0
  37. marimo_cad-0.1.0/tests/e2e/cad-viewer.spec.js-snapshots/001-initial-render-chromium-darwin.png +0 -0
  38. marimo_cad-0.1.0/tests/e2e/cad-viewer.spec.js-snapshots/002-dynamic-parts-chromium-darwin.png +0 -0
  39. marimo_cad-0.1.0/tests/e2e/cad-viewer.spec.js-snapshots/003-selection-chromium-darwin.png +0 -0
  40. marimo_cad-0.1.0/tests/e2e/cad-viewer.spec.js-snapshots/004-visibility-toggle-chromium-darwin.png +0 -0
  41. marimo_cad-0.1.0/tests/e2e/cad-viewer.spec.js-snapshots/005-camera-preserved-chromium-darwin.png +0 -0
  42. marimo_cad-0.1.0/tests/e2e/cad-viewer.spec.js-snapshots/006-clipping-initial-chromium-darwin.png +0 -0
  43. marimo_cad-0.1.0/tests/e2e/cad-viewer.spec.js-snapshots/007-clipping-dynamic-chromium-darwin.png +0 -0
  44. marimo_cad-0.1.0/tests/test_export.py +120 -0
  45. marimo_cad-0.1.0/tests/test_tessellate.py +192 -0
  46. marimo_cad-0.1.0/tests/test_widget.py +164 -0
  47. marimo_cad-0.1.0/uv.lock +2094 -0
@@ -0,0 +1,56 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v4
21
+
22
+ - name: Set up Node.js
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: "20"
26
+
27
+ - name: Build JS bundle
28
+ run: |
29
+ cd js
30
+ npm install
31
+ npm run build
32
+
33
+ - name: Build package
34
+ run: uv build
35
+
36
+ - name: Upload dist
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+
42
+ publish:
43
+ needs: build
44
+ runs-on: ubuntu-latest
45
+ environment: pypi
46
+ permissions:
47
+ id-token: write
48
+ steps:
49
+ - name: Download dist
50
+ uses: actions/download-artifact@v4
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+
55
+ - name: Publish to PyPI
56
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,57 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ .venv/
23
+ venv/
24
+ ENV/
25
+
26
+ # Node
27
+ node_modules/
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+
32
+ # IDE
33
+ .idea/
34
+ .vscode/
35
+ *.swp
36
+ *.swo
37
+ *~
38
+
39
+ # OS
40
+ .DS_Store
41
+ Thumbs.db
42
+
43
+ # Project specific
44
+ src/marimo_cad/static/widget.js
45
+ src/marimo_cad/static/widget.css
46
+
47
+ # Playwright
48
+ test-results/
49
+ playwright-report/
50
+ blob-report/
51
+ .playwright/
52
+
53
+ # Marimo cache
54
+ **/__marimo__/
55
+
56
+ # Keep sample files but ignore generated exports
57
+ !notebooks/sample_part.step
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cemrehan Cavdar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: marimo-cad
3
+ Version: 0.1.0
4
+ Summary: Reactive Parametric CAD for marimo notebooks
5
+ Project-URL: Homepage, https://github.com/cemrehancavdar/marimo-cad
6
+ Project-URL: Repository, https://github.com/cemrehancavdar/marimo-cad
7
+ Project-URL: Issues, https://github.com/cemrehancavdar/marimo-cad/issues
8
+ Author-email: Cemrehan Cavdar <cemrehancavdar@hotmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: 3d,anywidget,build123d,cad,marimo,parametric
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Visualization
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: anywidget>=0.9.0
24
+ Requires-Dist: build123d>=0.10.0
25
+ Requires-Dist: marimo>=0.9.0
26
+ Requires-Dist: ocp-tessellate>=3.0.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest; extra == 'dev'
29
+ Requires-Dist: ruff; extra == 'dev'
30
+ Provides-Extra: gears
31
+ Requires-Dist: bd-warehouse>=0.1.0; extra == 'gears'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # marimo-cad
35
+
36
+ **Reactive Parametric CAD** for [marimo](https://marimo.io) notebooks.
37
+
38
+ Build interactive 3D CAD models with sliders that update in real-time without losing your camera position.
39
+
40
+ ![Parametric Bookshelf Demo](assets/demo.gif)
41
+
42
+ ## Why marimo-cad?
43
+
44
+ | Use Case | Native build123d | marimo-cad |
45
+ |----------|------------------|------------|
46
+ | Quick visualization | Just return the object | Overkill |
47
+ | **Parametric design with sliders** | Camera resets on every change | Camera preserved |
48
+ | Named multi-part assemblies | No | Yes, with tree view |
49
+ | Export (STL/STEP/GLTF) | Manual | Built-in |
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ uv add marimo-cad
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ ```python
60
+ import marimo as mo
61
+ from build123d import Box
62
+ import marimo_cad as cad
63
+
64
+ size = mo.ui.slider(10, 50, value=20, label="Size")
65
+ viewer = cad.Viewer()
66
+
67
+ box = Box(size.value, size.value, size.value)
68
+ viewer.render(box)
69
+
70
+ mo.vstack([size, viewer])
71
+ ```
72
+
73
+ ## Examples
74
+
75
+ See [notebooks/](notebooks/) for complete examples:
76
+ - `bookshelf.py` - Parametric bookshelf
77
+ - `vase.py` - Parametric vase with STL export
78
+
79
+ ## Viewer Features
80
+
81
+ - **Mouse**: Rotate (drag), pan (right-drag), zoom (scroll)
82
+ - **Tree view**: Toggle part visibility
83
+ - **Clipping planes**: Slice along X/Y/Z
84
+ - **Measurement**: Distance and angles
85
+
86
+ ## Limitations
87
+
88
+ - Large models (>100k triangles) may be slow to tessellate
89
+ - Selection events not yet exposed to Python
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ uv sync && cd js && npm install && npm run build && cd ..
95
+ uv run pytest tests/ # 34 tests
96
+ uv run ruff check src/ --fix # Lint
97
+ ```
98
+
99
+ ---
100
+
101
+ ## API Reference
102
+
103
+ ### Viewer
104
+
105
+ ```python
106
+ viewer = cad.Viewer(width="100%", height=600)
107
+ viewer.render(shapes)
108
+ ```
109
+
110
+ | Parameter | Type | Default | Description |
111
+ |-----------|------|---------|-------------|
112
+ | `width` | `str \| int` | `"100%"` | CSS width or pixels |
113
+ | `height` | `int` | `600` | Height in pixels |
114
+
115
+ **render(shapes)** - Render shapes, preserving camera position.
116
+
117
+ ```python
118
+ # Single shape
119
+ viewer.render(box)
120
+
121
+ # Multiple shapes
122
+ viewer.render([box, cylinder])
123
+
124
+ # Named parts with colors
125
+ viewer.render([
126
+ {"shape": base, "name": "Base", "color": "blue"},
127
+ {"shape": top, "name": "Top", "color": "red", "alpha": 0.8},
128
+ ])
129
+ ```
130
+
131
+ ### PartSpec
132
+
133
+ Dict for specifying parts with metadata:
134
+
135
+ | Key | Type | Required | Description |
136
+ |-----|------|----------|-------------|
137
+ | `shape` | `Shape` | Yes | build123d object |
138
+ | `name` | `str` | No | Display name |
139
+ | `color` | `str` | No | Color name or hex |
140
+ | `alpha` | `float` | No | Opacity 0.0-1.0 |
141
+
142
+ ### COLORS
143
+
144
+ Available named colors: `blue`, `red`, `green`, `yellow`, `orange`, `purple`, `cyan`, `pink`, `gray`, `white`, `black`
145
+
146
+ ```python
147
+ {"shape": box, "color": "blue"} # Named
148
+ {"shape": box, "color": "#ff6600"} # Hex
149
+ ```
150
+
151
+ ### Export Functions
152
+
153
+ ```python
154
+ from marimo_cad import export_stl, export_step, export_gltf
155
+
156
+ export_stl(obj, "part.stl") # 3D printing
157
+ export_stl(obj, "fine.stl", tolerance=0.0001) # Higher resolution
158
+ export_step(obj, "part.step") # CAD interchange (lossless)
159
+ export_gltf(obj, "part.glb") # Web viewers
160
+ ```
161
+
162
+ | Function | Parameters | Description |
163
+ |----------|------------|-------------|
164
+ | `export_stl` | `obj, filename, tolerance=0.001, angular_tolerance=0.1` | Tessellated mesh |
165
+ | `export_step` | `obj, filename` | Exact geometry |
166
+ | `export_gltf` | `obj, filename` | Web-ready format |
167
+
168
+ All return `Path` to exported file.
169
+
170
+ ### How It Works
171
+
172
+ 1. **Tessellate**: build123d → triangle meshes (ocp-tessellate)
173
+ 2. **Transport**: Mesh data → JavaScript (anywidget)
174
+ 3. **Render**: Three.js via three-cad-viewer
175
+ 4. **Update**: `render()` updates geometry, camera preserved
176
+
177
+ ## License
178
+
179
+ MIT
@@ -0,0 +1,146 @@
1
+ # marimo-cad
2
+
3
+ **Reactive Parametric CAD** for [marimo](https://marimo.io) notebooks.
4
+
5
+ Build interactive 3D CAD models with sliders that update in real-time without losing your camera position.
6
+
7
+ ![Parametric Bookshelf Demo](assets/demo.gif)
8
+
9
+ ## Why marimo-cad?
10
+
11
+ | Use Case | Native build123d | marimo-cad |
12
+ |----------|------------------|------------|
13
+ | Quick visualization | Just return the object | Overkill |
14
+ | **Parametric design with sliders** | Camera resets on every change | Camera preserved |
15
+ | Named multi-part assemblies | No | Yes, with tree view |
16
+ | Export (STL/STEP/GLTF) | Manual | Built-in |
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ uv add marimo-cad
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```python
27
+ import marimo as mo
28
+ from build123d import Box
29
+ import marimo_cad as cad
30
+
31
+ size = mo.ui.slider(10, 50, value=20, label="Size")
32
+ viewer = cad.Viewer()
33
+
34
+ box = Box(size.value, size.value, size.value)
35
+ viewer.render(box)
36
+
37
+ mo.vstack([size, viewer])
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ See [notebooks/](notebooks/) for complete examples:
43
+ - `bookshelf.py` - Parametric bookshelf
44
+ - `vase.py` - Parametric vase with STL export
45
+
46
+ ## Viewer Features
47
+
48
+ - **Mouse**: Rotate (drag), pan (right-drag), zoom (scroll)
49
+ - **Tree view**: Toggle part visibility
50
+ - **Clipping planes**: Slice along X/Y/Z
51
+ - **Measurement**: Distance and angles
52
+
53
+ ## Limitations
54
+
55
+ - Large models (>100k triangles) may be slow to tessellate
56
+ - Selection events not yet exposed to Python
57
+
58
+ ## Development
59
+
60
+ ```bash
61
+ uv sync && cd js && npm install && npm run build && cd ..
62
+ uv run pytest tests/ # 34 tests
63
+ uv run ruff check src/ --fix # Lint
64
+ ```
65
+
66
+ ---
67
+
68
+ ## API Reference
69
+
70
+ ### Viewer
71
+
72
+ ```python
73
+ viewer = cad.Viewer(width="100%", height=600)
74
+ viewer.render(shapes)
75
+ ```
76
+
77
+ | Parameter | Type | Default | Description |
78
+ |-----------|------|---------|-------------|
79
+ | `width` | `str \| int` | `"100%"` | CSS width or pixels |
80
+ | `height` | `int` | `600` | Height in pixels |
81
+
82
+ **render(shapes)** - Render shapes, preserving camera position.
83
+
84
+ ```python
85
+ # Single shape
86
+ viewer.render(box)
87
+
88
+ # Multiple shapes
89
+ viewer.render([box, cylinder])
90
+
91
+ # Named parts with colors
92
+ viewer.render([
93
+ {"shape": base, "name": "Base", "color": "blue"},
94
+ {"shape": top, "name": "Top", "color": "red", "alpha": 0.8},
95
+ ])
96
+ ```
97
+
98
+ ### PartSpec
99
+
100
+ Dict for specifying parts with metadata:
101
+
102
+ | Key | Type | Required | Description |
103
+ |-----|------|----------|-------------|
104
+ | `shape` | `Shape` | Yes | build123d object |
105
+ | `name` | `str` | No | Display name |
106
+ | `color` | `str` | No | Color name or hex |
107
+ | `alpha` | `float` | No | Opacity 0.0-1.0 |
108
+
109
+ ### COLORS
110
+
111
+ Available named colors: `blue`, `red`, `green`, `yellow`, `orange`, `purple`, `cyan`, `pink`, `gray`, `white`, `black`
112
+
113
+ ```python
114
+ {"shape": box, "color": "blue"} # Named
115
+ {"shape": box, "color": "#ff6600"} # Hex
116
+ ```
117
+
118
+ ### Export Functions
119
+
120
+ ```python
121
+ from marimo_cad import export_stl, export_step, export_gltf
122
+
123
+ export_stl(obj, "part.stl") # 3D printing
124
+ export_stl(obj, "fine.stl", tolerance=0.0001) # Higher resolution
125
+ export_step(obj, "part.step") # CAD interchange (lossless)
126
+ export_gltf(obj, "part.glb") # Web viewers
127
+ ```
128
+
129
+ | Function | Parameters | Description |
130
+ |----------|------------|-------------|
131
+ | `export_stl` | `obj, filename, tolerance=0.001, angular_tolerance=0.1` | Tessellated mesh |
132
+ | `export_step` | `obj, filename` | Exact geometry |
133
+ | `export_gltf` | `obj, filename` | Web-ready format |
134
+
135
+ All return `Path` to exported file.
136
+
137
+ ### How It Works
138
+
139
+ 1. **Tessellate**: build123d → triangle meshes (ocp-tessellate)
140
+ 2. **Transport**: Mesh data → JavaScript (anywidget)
141
+ 3. **Render**: Three.js via three-cad-viewer
142
+ 4. **Update**: `render()` updates geometry, camera preserved
143
+
144
+ ## License
145
+
146
+ MIT
Binary file
@@ -0,0 +1,214 @@
1
+ # JS Architecture: three-cad-viewer Wrapper
2
+
3
+ ## Overview
4
+
5
+ This document explains the JavaScript wrapper layer we built over `three-cad-viewer` to enable reactive CAD visualization in marimo notebooks.
6
+
7
+ ## The Problem
8
+
9
+ `three-cad-viewer` is a powerful CAD viewer, but it has several quirks that make reactive updates difficult:
10
+
11
+ 1. **Full re-render resets camera** - Calling `viewer.render()` rebuilds everything and resets camera position
12
+ 2. **Internal API confusion** - `ObjectGroup` extends `THREE.Group` directly, so position/quaternion are on the group itself (not `group.group`)
13
+ 3. **Path-based group keys** - Parts are stored with paths like `/shapes/assembly/Left` not just `Left`
14
+ 4. **Tree view coupling** - The tree UI is built during render and doesn't update when parts change
15
+ 5. **No exported TreeView** - The `TreeView` class is internal, making tree updates tricky
16
+
17
+ ## Solution Architecture
18
+
19
+ ```
20
+ ┌─────────────────────────────────────────────────────────────┐
21
+ │ index.js (Widget) │
22
+ │ - Creates Display and LiveViewer │
23
+ │ - Handles model.on('change:shapes_data') events │
24
+ │ - Calls viewer.syncParts() for updates │
25
+ └─────────────────────────────────────────────────────────────┘
26
+
27
+
28
+ ┌─────────────────────────────────────────────────────────────┐
29
+ │ LiveViewer (extends Viewer) │
30
+ │ - render() - initial render, builds PartManager │
31
+ │ - syncParts() - smart update (add/remove/update parts) │
32
+ │ - _rebuildTreeView() - updates tree when parts change │
33
+ │ - Exposes parts: PartManager for direct manipulation │
34
+ └─────────────────────────────────────────────────────────────┘
35
+
36
+
37
+ ┌─────────────────────────────────────────────────────────────┐
38
+ │ PartManager │
39
+ │ - get(id) → PartHandle │
40
+ │ - add(partData) → PartHandle │
41
+ │ - remove(id) │
42
+ │ - update(id, partData) │
43
+ │ - sync(partsArray) → { added, updated, removed } │
44
+ │ - buildFromViewer() - maps viewer's groups to handles │
45
+ └─────────────────────────────────────────────────────────────┘
46
+
47
+
48
+ ┌─────────────────────────────────────────────────────────────┐
49
+ │ PartHandle │
50
+ │ - setPosition(x, y, z) │
51
+ │ - setQuaternion(x, y, z, w) │
52
+ │ - setTransform(loc) - from [[pos], [quat]] format │
53
+ │ - updateGeometry(vertices, normals, triangles) │
54
+ │ - updateEdges(edgeData) │
55
+ │ - dispose() │
56
+ └─────────────────────────────────────────────────────────────┘
57
+
58
+
59
+ ┌─────────────────────────────────────────────────────────────┐
60
+ │ three-cad-viewer (external library) │
61
+ │ - Viewer: base viewer class │
62
+ │ - Display: UI container (tree, toolbar, canvas) │
63
+ │ - THREE.js scene management │
64
+ └─────────────────────────────────────────────────────────────┘
65
+ ```
66
+
67
+ ## Key Files
68
+
69
+ ### `js/src/part-manager.js`
70
+
71
+ **PartHandle** - Clean interface to a single part:
72
+ - Abstracts away three-cad-viewer's ObjectGroup quirks
73
+ - Provides simple `setPosition()`, `updateGeometry()` methods
74
+ - Handles buffer reuse for efficient geometry updates
75
+
76
+ **PartManager** - Manages all parts:
77
+ - Tracks parts by ID with a Map
78
+ - `sync()` method does intelligent diffing: only adds new, removes old, updates existing
79
+ - `buildFromViewer()` maps three-cad-viewer's internal groups to PartHandles
80
+ - Handles path-based key lookup (finds `Left` in `/shapes/assembly/Left`)
81
+
82
+ ### `js/src/live-viewer.js`
83
+
84
+ **LiveViewer** - Extends `three-cad-viewer.Viewer`:
85
+ - Overrides `render()` to initialize PartManager after scene builds
86
+ - `syncParts()` - main update method, preserves camera
87
+ - `_rebuildTreeView()` - reconstructs tree UI when parts change
88
+ - `_buildTreeFromParts()` - creates tree data structure from parts array
89
+
90
+ ### `js/src/index.js`
91
+
92
+ **Widget entry point**:
93
+ - Creates Display and LiveViewer
94
+ - Listens for `change:shapes_data` traitlet events
95
+ - First render uses `viewer.render()`, subsequent updates use `viewer.syncParts()`
96
+
97
+ ## How It Works
98
+
99
+ ### Initial Render
100
+ ```javascript
101
+ // First time - full render
102
+ viewer.render(shapesData, renderOptions, viewerOptions);
103
+ // This:
104
+ // 1. Builds THREE.js scene with all parts
105
+ // 2. Creates tree view
106
+ // 3. Sets up camera, lights, controls
107
+ // 4. PartManager.buildFromViewer() maps groups to handles
108
+ ```
109
+
110
+ ### Subsequent Updates
111
+ ```javascript
112
+ // When slider changes - smart sync
113
+ viewer.syncParts(newShapesData);
114
+ // This:
115
+ // 1. PartManager.sync() diffs old vs new parts
116
+ // 2. Removes parts not in new data
117
+ // 3. Updates existing parts (geometry + transform)
118
+ // 4. Adds new parts
119
+ // 5. If parts added/removed, rebuilds tree view
120
+ // 6. Camera position preserved!
121
+ ```
122
+
123
+ ### Tree View Rebuild
124
+ ```javascript
125
+ // When parts added or removed
126
+ _rebuildTreeView(partsData) {
127
+ // 1. Build tree structure: { shapes: { "PartName": [1,1], ... } }
128
+ const newTree = this._buildTreeFromParts(partsData);
129
+
130
+ // 2. Update viewer's internal tree properties
131
+ this.tree = this.expandedTree = this.compactTree = newTree;
132
+
133
+ // 3. Get TreeView class from existing instance (not exported)
134
+ const TreeViewClass = this.treeview.constructor;
135
+
136
+ // 4. Dispose old, create new
137
+ this.treeview.dispose();
138
+ this.treeview = new TreeViewClass(this.tree, ...callbacks);
139
+
140
+ // 5. Render new tree
141
+ this.display.clearCadTree();
142
+ this.display.addCadTree(this.treeview.create());
143
+ this.treeview.render();
144
+ }
145
+ ```
146
+
147
+ ## three-cad-viewer Quirks Handled
148
+
149
+ ### ObjectGroup is a THREE.Group
150
+ ```javascript
151
+ // WRONG - group.group is undefined
152
+ group.group.position.set(x, y, z);
153
+
154
+ // CORRECT - ObjectGroup extends THREE.Group
155
+ group.position.set(x, y, z);
156
+ ```
157
+
158
+ ### Path-based Keys
159
+ ```javascript
160
+ // Groups are stored with paths
161
+ const groups = viewer.nestedGroup.groups;
162
+ // Keys: "/shapes/assembly/Left", "/shapes/assembly/Right", etc.
163
+
164
+ // PartManager handles lookup
165
+ _findViewerGroup(id) {
166
+ for (const [path, group] of Object.entries(groups)) {
167
+ if (path === id || path.endsWith('/' + id)) {
168
+ return { path, group };
169
+ }
170
+ }
171
+ }
172
+ ```
173
+
174
+ ### Tree Structure Format
175
+ ```javascript
176
+ // three-cad-viewer expects this format:
177
+ {
178
+ "shapes": {
179
+ "Left Side": [1, 1], // [shape_visible, edges_visible]
180
+ "Right Side": [1, 1],
181
+ "Top": [1, 1],
182
+ // ...
183
+ }
184
+ }
185
+ ```
186
+
187
+ ## Usage Example
188
+
189
+ ```javascript
190
+ // In marimo widget
191
+ const viewer = new LiveViewer(display, options, callback);
192
+
193
+ // Initial render
194
+ viewer.render(shapesData, renderOptions, viewerOptions);
195
+
196
+ // When data changes (slider moved)
197
+ model.on("change:shapes_data", () => {
198
+ const newData = model.get("shapes_data");
199
+ viewer.syncParts(newData); // Camera preserved, tree updated
200
+ });
201
+
202
+ // Direct part manipulation (if needed)
203
+ const part = viewer.getPart("Left Side");
204
+ part.setPosition(10, 0, 0);
205
+ viewer.update(true);
206
+ ```
207
+
208
+ ## Benefits
209
+
210
+ 1. **Clean API** - No need to understand three-cad-viewer internals
211
+ 2. **Reactive updates** - `syncParts()` handles add/remove/update intelligently
212
+ 3. **Camera preserved** - No jarring resets when parameters change
213
+ 4. **Tree updates** - Tree view reflects current parts
214
+ 5. **Efficient** - Reuses geometry buffers when sizes match