Skip to content

Conversation

jtydhr88
Copy link
Collaborator

@jtydhr88 jtydhr88 commented Sep 25, 2025

Summary

Fully Refactored the Load3D module to improve architecture and maintainability by consolidating functionality into a
centralized composable pattern and simplifying component structure. and support VueNodes system

Changes

  • Architecture: Introduced new useLoad3d composable to centralize 3D loading logic and state
    management
  • Component Simplification: Removed redundant components (Load3DAnimation.vue, Load3DAnimationScene.vue,
    PreviewManager.ts)
  • Support VueNodes
  • improve config store
  • remove lineart output due Animation doesnot support it, may add it back later
  • remove Preview screen and keep scene in fixed ratio in load3d (not affect preview3d)
  • improve record video feature which will already record video by same ratio as scene
    Need BE change Merge 3d animation node comfyanonymous/ComfyUI#10025
2025-09-25.22-33-36.mp4

┆Issue is synchronized with this Notion page by Unito

@jtydhr88 jtydhr88 marked this pull request as draft September 25, 2025 01:57
@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Sep 25, 2025
Copy link

github-actions bot commented Sep 25, 2025

🎭 Playwright Test Results

⚠️ Tests passed with flaky tests

⏰ Completed at: 10/08/2025, 07:06:42 PM UTC

📈 Summary

  • Total Tests: 488
  • Passed: 455 ✅
  • Failed: 0
  • Flaky: 3 ⚠️
  • Skipped: 30 ⏭️

📊 Test Reports by Browser

  • chromium: View Report • ✅ 446 / ❌ 0 / ⚠️ 3 / ⏭️ 30
  • chromium-2x: View Report • ✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0
  • chromium-0.5x: View Report • ✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0
  • mobile-chrome: View Report • ✅ 6 / ❌ 0 / ⚠️ 0 / ⏭️ 0

🎉 Click on the links above to view detailed test results for each browser configuration.

@jtydhr88 jtydhr88 force-pushed the refactor-load3d-2 branch 3 times, most recently from 5b73fbc to 40f4e5d Compare September 26, 2025 02:36
@jtydhr88 jtydhr88 marked this pull request as ready for review September 26, 2025 02:42
christian-byrne pushed a commit that referenced this pull request Sep 30, 2025
…el events (#5821)

## Summary

Add generic wheel event capture mechanism for interactive widgets in
vueNodes system to prevent event bubbling to canvas.

  ## Changes

- What: Add event handling logic in LGraphNode.vue and GraphCanvas.vue
that checks for data-capture-wheel attribute to determine whether wheel
events should be forwarded to the canvas
- How it works: Components that need to capture wheel events (like
Three.js scenes) can add data-capture-wheel="true" attribute to prevent
wheel events from bubbling up to the canvas zoom handler

prerequirist for #5765

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5821-Check-if-the-wheel-event-is-from-an-element-that-wants-to-capture-wheel-events-27b6d73d3650812493b5f13849147e6c)
by [Unito](https://www.unito.io)
@christian-byrne christian-byrne added claude-review Add to trigger a PR code review from Claude Code and removed claude-review Add to trigger a PR code review from Claude Code labels Sep 30, 2025
@christian-byrne
Copy link
Contributor

Could you fix the merge conflict then rebase with main?

Copy link

github-actions bot commented Sep 30, 2025

🎨 Storybook Build Status

Build completed successfully!

⏰ Completed at: 10/08/2025, 06:47:44 PM UTC

🔗 Links


🎉 Your Storybook is ready for review!

@christian-byrne christian-byrne added the claude-review Add to trigger a PR code review from Claude Code label Oct 1, 2025
claude[bot]

This comment was marked as outdated.

Comment on lines 175 to 179
const widthWidget = node.widgets?.find((w) => w.name === 'width')
const heightWidget = node.widgets?.find((w) => w.name === 'height')
if (!(widthWidget && heightWidget)) {
isPreview.value = true
}
Copy link
Contributor

Choose a reason for hiding this comment

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

question: what is the logic for this? If there is not a width and a height widget, then it means it's a preview node?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, and it will be used in, for example, if open 3d viewer from preview3D, drag and drop feature will be disabled.

Copy link
Contributor

Choose a reason for hiding this comment

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

What is the heuristic? Why is it the case that, if there is no width and no height, it means it's a preview widget? It seems a bit hard to understand.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Width and height refer to the output size in load3d. They only exist in load3d, not in preview3d. However, since load3d and preview3d now share this part of the logic, and certain features (such as drag and drop file) only work for load3d, I need to use them to identify whether the source is load3d or preview3d to differentiate between them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

image

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just use the node type? You already have a reference to the node. This current setup seems hard to maintain - what happens if you want to change the name of the width widget in 6 months? Will you remember that it's going to break this logic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good catch, yes, we can. Just updated

christian-byrne pushed a commit that referenced this pull request Oct 6, 2025
…el events (#5821)

## Summary

Add generic wheel event capture mechanism for interactive widgets in
vueNodes system to prevent event bubbling to canvas.

  ## Changes

- What: Add event handling logic in LGraphNode.vue and GraphCanvas.vue
that checks for data-capture-wheel attribute to determine whether wheel
events should be forwarded to the canvas
- How it works: Components that need to capture wheel events (like
Three.js scenes) can add data-capture-wheel="true" attribute to prevent
wheel events from bubbling up to the canvas zoom handler

prerequirist for #5765

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5821-Check-if-the-wheel-event-is-from-an-element-that-wants-to-capture-wheel-events-27b6d73d3650812493b5f13849147e6c)
by [Unito](https://www.unito.io)
christian-byrne pushed a commit that referenced this pull request Oct 6, 2025
## Summary

pass nodeId to widget component, which is a prerequirist for
#5765

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5853-pass-nodeData-to-widget-component-27e6d73d3650811e9e48ceb7c3a00545)
by [Unito](https://www.unito.io)
arjansingh pushed a commit that referenced this pull request Oct 7, 2025
…el events (#5821)

## Summary

Add generic wheel event capture mechanism for interactive widgets in
vueNodes system to prevent event bubbling to canvas.

  ## Changes

- What: Add event handling logic in LGraphNode.vue and GraphCanvas.vue
that checks for data-capture-wheel attribute to determine whether wheel
events should be forwarded to the canvas
- How it works: Components that need to capture wheel events (like
Three.js scenes) can add data-capture-wheel="true" attribute to prevent
wheel events from bubbling up to the canvas zoom handler

prerequirist for #5765

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5821-Check-if-the-wheel-event-is-from-an-element-that-wants-to-capture-wheel-events-27b6d73d3650812493b5f13849147e6c)
by [Unito](https://www.unito.io)
arjansingh pushed a commit that referenced this pull request Oct 7, 2025
## Summary

pass nodeId to widget component, which is a prerequirist for
#5765

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5853-pass-nodeData-to-widget-component-27e6d73d3650811e9e48ceb7c3a00545)
by [Unito](https://www.unito.io)
Copy link

github-actions bot commented Oct 8, 2025

🔧 Auto-fixes Applied

This PR has been automatically updated to fix linting and formatting issues.

⚠️ Important: Your local branch is now behind. Run git pull before making additional changes to avoid conflicts.

Changes made:

  • ESLint auto-fixes
  • Prettier formatting

Copy link
Contributor

@christian-byrne christian-byrne left a comment

Choose a reason for hiding this comment

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

I have reviewed 14/42 files, taking a quick break.

Comment on lines +37 to +60
v-if="activeCategory === 'scene' && sceneConfig"
ref="sceneControlsRef"
:background-color="backgroundColor"
:show-grid="showGrid"
:has-background-image="hasBackgroundImage"
@toggle-grid="handleToggleGrid"
@update-background-color="handleBackgroundColorChange"
v-model:show-grid="sceneConfig.showGrid"
v-model:background-color="sceneConfig.backgroundColor"
v-model:background-image="sceneConfig.backgroundImage"
@update-background-image="handleBackgroundImageUpdate"
/>

<ModelControls
v-if="activeCategory === 'model'"
v-if="activeCategory === 'model' && modelConfig"
ref="modelControlsRef"
:input-spec="inputSpec"
:up-direction="upDirection"
:material-mode="materialMode"
:edge-threshold="edgeThreshold"
@update-up-direction="handleUpdateUpDirection"
@update-material-mode="handleUpdateMaterialMode"
@update-edge-threshold="handleUpdateEdgeThreshold"
v-model:material-mode="modelConfig.materialMode"
v-model:up-direction="modelConfig.upDirection"
/>

<CameraControls
v-if="activeCategory === 'camera'"
v-if="activeCategory === 'camera' && cameraConfig"
ref="cameraControlsRef"
:camera-type="cameraType"
:fov="fov"
:show-f-o-v-button="showFOVButton"
@switch-camera="switchCamera"
@update-f-o-v="handleUpdateFOV"
v-model:camera-type="cameraConfig.cameraType"
v-model:fov="cameraConfig.fov"
/>

<LightControls
v-if="activeCategory === 'light'"
v-if="activeCategory === 'light' && lightConfig && modelConfig"
Copy link
Contributor

Choose a reason for hiding this comment

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

for the v-if on the Controls, you can use computed to prevent re-render on every change of activeCategory and config.

// Drag and drop state
const isDragging = ref(false)
const dragMessage = ref('')
const SUPPORTED_EXTENSIONS = ['.gltf', '.glb', '.obj', '.fbx', '.stl']
Copy link
Contributor

Choose a reason for hiding this comment

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

question: we are defining this in both Load3dViewerContent and Load3DScene, are they always going to be the same? If so, can we put it in a constants file that both places import from?

Comment on lines +119 to +157
function handleDragOver(event: DragEvent) {
if (viewer.isPreview.value) return
if (!event.dataTransfer) return
const hasFiles = event.dataTransfer.types.includes('Files')
if (!hasFiles) return
isDragging.value = true
event.dataTransfer.dropEffect = 'copy'
dragMessage.value = t('load3d.dropToLoad')
}
function handleDragLeave() {
isDragging.value = false
}
async function handleDrop(event: DragEvent) {
isDragging.value = false
if (viewer.isPreview.value) return
if (!event.dataTransfer) return
const files = Array.from(event.dataTransfer.files)
if (files.length === 0) return
const modelFile = files.find(isValidModelFile)
if (modelFile) {
await viewer.handleModelDrop(modelFile)
} else {
useToastStore().addAlert(t('load3d.unsupportedFileType'))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: This seems like something that could be moved into a hook and re-used in other components.


try {
load3d = new Load3d(containerRef, {
node: node as LGraphNode
Copy link
Contributor

Choose a reason for hiding this comment

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

question: is this type assertion still necessary if we add null check above? If so, can we use generic toRaw<LGraphNode>?

Copy link
Contributor

Choose a reason for hiding this comment

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

Same question for all the below type assertions as LGraphNode as well.

}

node.onRemoved = function () {
useLoad3dService().removeLoad3d(node as LGraphNode)
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we also need to clear entries in nodeToLoad3dMap and pendingCallbacks here?

}
}

const restoreConfigurationsFromNode = async (node: LGraphNode) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit (non-blocking): all of the logic revolving around serialization, config management, config types, and restoring from configs can likely be moved to its own module.

Comment on lines +168 to +192
if (modelWidget?.value) {
const modelUrl = getModelUrl(modelWidget.value as string)
if (modelUrl) {
loading.value = true
loadingMessage.value = t('load3d.reloadingModel')
try {
await load3d.loadModel(modelUrl)

if (cameraStateToRestore) {
await nextTick()
load3d.setCameraState(cameraStateToRestore)
}
} catch (error) {
console.error('Failed to reload model:', error)
useToastStore().addAlert(t('toastMessages.failedToLoadModel'))
} finally {
loading.value = false
loadingMessage.value = ''
}
}
} else if (cameraStateToRestore) {
load3d.setCameraState(cameraStateToRestore)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit (non-blocking, suggestion for future): We should consider whether this needs to be blocking setup. Currently, we are synchronously restoring the model config, get the url, loading the model, setting the camera state - and everything else must wait on this to happen. I believe we could refactor a bit and have this happen in the background like with useAsyncState

pendingCallbacks.get(node as LGraphNode)!.push(callback)
}

watch(
Copy link
Contributor

Choose a reason for hiding this comment

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

question: it seems that we have these watchers which will dynamically handle updates to the config objects. But we also have the same logic (as far as I can tell) repeated in restoreConfigurationsFromNode. Is that correct?

For example, this watcher already handles updates to the CameraConfig:

  watch(
    cameraConfig,
    (newValue) => {
      if (load3d && nodeRef.value) {
        nodeRef.value.properties['Camera Config'] = newValue
        load3d.toggleCamera(newValue.cameraType)
        load3d.setFOV(newValue.fov)
      }
    },
    { deep: true }
  )

But we are also doing the same thing in the restoreConfigurationsFromNode:

    const savedCameraConfig = node.properties['Camera Config'] as CameraConfig
    const cameraStateToRestore = savedCameraConfig?.state

    // Apply camera type and FOV immediately
    if (savedCameraConfig) {
      cameraConfig.value = savedCameraConfig
      load3d.toggleCamera(savedCameraConfig.cameraType)
      load3d.setFOV(savedCameraConfig.fov)
    }

Shouldn't this line

cameraConfig.value = savedCameraConfig

actually be triggering that watch, making the next two lines redundant?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-review Add to trigger a PR code review from Claude Code size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants