Skip to content

Communication issue between trame and vue3 applicationΒ #7

@MarcoAstaTEORESI

Description

@MarcoAstaTEORESI

Hi, i'm trying to communicate some data from vue3 component to trame but it's seems is not working.
Following vue3 component:

<script setup lang="ts">
  import { ref, onMounted, onUnmounted, computed } from 'vue'
  import sharedConfig from '@pomini-apps/shared/src/core/sharedConfig'
  import { ModuleMediaRequest } from '@pomini-apps/shared/src/types/api-mediaservice'

  interface Props {
    req: ModuleMediaRequest
  }

  const props = defineProps<Props>()
  const iframeRef = ref<HTMLIFrameElement | null>(null)
  const receivedMessages = ref<string[]>([])

  const iframeSrc = computed(() => {
    if (sharedConfig.nodeEnv === 'development') return sharedConfig.urlMfePyTrame
    if (sharedConfig.nodeEnv === 'production') return `${window.location.origin}/pytrame/`
    throw new Error('getUrlPyTrame nodeEnv not provided :' + sharedConfig.nodeEnv)
  })

  const sendParametersToIframe = () => {
    if (!iframeRef.value?.contentWindow) return

    const messageObject = {
      type: 'VTU_PARAMETERS',
      data: {
        id: props.req.id,
        plantId: props.req.plantId,
        productionId: props.req.productionId,
        machineId: props.req.machineId,
        module: props.req.module,
        mediaType: props.req.mediaType,
        mediaFormat: props.req.mediaFormat,
        language: props.req.language,
        presetId: props.req.presetId,
        uiInfo: props.req.uiInfo,
        timestamp: new Date().toISOString()
      }
    }

    iframeRef.value.contentWindow.postMessage(
      {
        emit: 'parent_to_child', // Matches Trame communicator event
        value: messageObject
      },
      '*'
    )

    console.log('Parameters sent to iframe:', messageObject)
  }

  // Handle messages FROM Trame iframe
  const handleMessageFromIframe = (event: MessageEvent) => {
    // Security: Check origin in production
    // if (event.origin !== 'https://your-trame-domain.com') return;

    if (!event.data || !event.data.emit) return

    console.log('πŸ“¨ Message received from iframe:', event.data)

    const message = `[${new Date().toLocaleTimeString()}] ${event.data.emit}: ${JSON.stringify(event.data.value)}`
    receivedMessages.value.unshift(message)
    if (receivedMessages.value.length > 10) receivedMessages.value.pop()

    switch (event.data.emit) {
      case 'child_to_parent':
        console.log('Child to parent message:', event.data.value)
        break
      case 'app_ready':
        console.log('πŸŽ‰ Trame app is ready!')
        sendParametersToIframe() // Only send after Trame signals ready
        break
      case 'vtu_loaded':
        console.log('βœ… VTU data loaded:', event.data.value)
        break
      case 'file_changed':
        console.log('πŸ“ File changed:', event.data.value)
        break
    }
  }

  onMounted(() => {
    window.addEventListener('message', handleMessageFromIframe)
  })

  onUnmounted(() => {
    window.removeEventListener('message', handleMessageFromIframe)
  })
</script>

<template>
  <div class="flex flex-col w-full h-full">
    <!-- Iframe container -->
    <div class="flex-1 relative">
      <iframe ref="iframeRef" :src="iframeSrc" class="w-full h-full border-none" title="VTU player" />
    </div>
  </div>
</template>

Following the python code:

"""Main VTU Player application class."""

import os
import sys
import signal
from typing import List, Dict, Optional, Tuple

from loguru import logger
from trame.app import get_server

from config.settings import config
from dataloader.dataloader import DataLoader
from visualization.vtk_pipeline import VTKPipeline
from visualization.enums import RenderMode, ColorMap
from ui.components import UIComponents
from ui.callbacks import CallbackManager

class VTUViewerApp:
    """Main VTU Player application."""

    def __init__(self):
        # --- Core services ---
        self.data_loader = DataLoader(config)
        self.vtk_pipeline = VTKPipeline(config)

        # --- Trame server ---
        self.server = get_server(client_type="vue3")
        self.state = self.server.state # type: ignore
        self.ctrl = self.server.controller # type: ignore

        # --- UI & callbacks ---
        self.ui_components = UIComponents(self.server, self.vtk_pipeline, config)
        self.callback_manager = CallbackManager(
            self.server, self.data_loader, self.vtk_pipeline
        )

        # --- Internal state ---
        self.dataset_arrays: List[Dict] = []
        self.default_array_range: Tuple[float, float] = (0.0, 1.0)
        self.contour_value: float = 0.5

        # --- Init ---
        logger.info('INIT')
        signal.signal(signal.SIGINT, self._signal_handler)
        self._init_state()

    # -------------------------------------------------------------------------
    # Init helpers
    # -------------------------------------------------------------------------
    def _init_state(self):
        """Initialize application state with defaults."""
        self.state.data_source = "remote"
        self.state.selected_file = ""
        self.state.available_files = []
        self.state.msg_received = []  # ensure it's always a list
 
        # Other initialization here...
        
    # -------------------------------------------------------------------------
    # Data initialization
    # -------------------------------------------------------------------------
    
    # Functions here for initialize data...

    # -------------------------------------------------------------------------
    # Communication handlers
    # -------------------------------------------------------------------------
    def parent_receive_msg(self, msg):
        """
        Handle messages received from parent iframe.
        Automatically unwraps the 'value' field sent from Vue.
        """
        # Unwrap value if message is wrapped
        if isinstance(msg, dict) and 'value' in msg:
            msg = msg['value']

        logger.info(f"πŸ“© Received message from parent: {msg}")

        # Keep last 10 messages
        if len(self.state.msg_received) >= 10:
            self.state.msg_received.pop()
        self.state.msg_received.insert(0, f"{msg}")
        
        # Trigger VTU parameter handler
        if isinstance(msg, dict) and msg.get("type") == "VTU_PARAMETERS":
            self._handle_vtu_parameters(msg.get("data", {}))

        self.state.dirty("msg_received")

    def _notify_parent_ready(self):
        """Notify parent window that app is ready."""
        logger.info("πŸš€ App ready, notifying parent window")
        self._send_to_parent({
            "type": "app_ready",  # This will be in event.data.value.type
            "data": {
                "selected_file": self.state.selected_file,
                "available_files": self.state.available_files,
                "timestamp": self._get_timestamp()
            }
        })

    def _handle_vtu_parameters(self, parameters: Dict):
        """Handle VTU parameters and load the corresponding data."""
        logger.info(f"βœ… VTU Parameters received: {parameters}")
        
        # Store parameters
        self.state.vtu_parameters = parameters
        
        # Load data based on parameters
        self.initialize_data_based_on_params(parameters)
        
        # Send acknowledgment back to parent
        self._send_to_parent({
            "type": "vtu_loaded",
            "data": {
                "status": "success",
                "plantId": parameters.get('plantId'),
                "productionId": parameters.get('productionId'),
                "selected_file": self.state.selected_file,
                "timestamp": self._get_timestamp()
            }
        })

    def _get_timestamp(self) -> str:
        """Get current timestamp string."""
        from datetime import datetime
        return datetime.now().isoformat()

    def _send_to_parent(self, message):
        """Send message to parent iframe."""
        if hasattr(self.ctrl, 'child_post_message'):
            try:
                # The event name should match what the Vue component expects
                self.ctrl.child_post_message([{
                    "emit": "child_to_parent",  # This is what Vue listens for
                    "value": message
                }])
                logger.info(f"πŸ“€ Sent to parent: {message.get('type', 'Unknown')}")
            except Exception as e:
                logger.warning(f"⚠️ Could not send to parent: {e}")

    # -------------------------------------------------------------------------
    # UI & Callbacks
    # -------------------------------------------------------------------------
    def _update_ui_state_after_loading(self):
        self.state.contour_value = self.contour_value
        self.state.contour_min = self.default_array_range[0]
        self.state.contour_max = self.default_array_range[1]
        self.state.contour_step = 0.01 * (
            self.default_array_range[1] - self.default_array_range[0]
        )
        self.state.array_list = self.dataset_arrays

    def setup_callbacks(self):
        self.callback_manager.setup_callbacks()

        @self.state.change("selected_file")
        def on_file_selected(selected_file, **_):
            if selected_file:
                try:
                    self.initialize_data(
                        use_local=self.state.data_source == "local",
                        filename=selected_file
                    )
                    self.ctrl.view_update()
                    
                    # Notify parent window of file change
                    self._send_to_parent({
                        "type": "file_changed",
                        "data": {"selected_file": selected_file}
                    })
                except Exception as e:
                    logger.error(f"❌ Error loading {selected_file}: {e}")

    def create_ui(self):
        """Create UI and attach iframe communicator."""
        # Pass the message handler to UIComponents
        self.ui_components.create_layout(
            iframe_message_handler=self.parent_receive_msg
        )

    # -------------------------------------------------------------------------
    # Runtime
    # -------------------------------------------------------------------------

    def run(self, use_local=True, filename=None, **server_kwargs):
        """Run the application."""
        try:
            # Default init (params may override later via postMessage)
            self.state.available_files = self.data_loader.get_available_files()
            self.initialize_data(use_local=use_local, filename=filename)

            self.create_ui()
            self.setup_callbacks()
            
            # Notify parent after UI is created
            self._notify_parent_ready()
            
            self.server.start(**server_kwargs)

        except KeyboardInterrupt:
            logger.info("πŸ‘‹ User interrupted")
        except Exception as e:
            logger.error(f"πŸ’₯ Fatal error: {e}")
            raise

    # -------------------------------------------------------------------------
    # Shutdown
    # -------------------------------------------------------------------------
    def _signal_handler(self, *_):
        logger.info("πŸ›‘ Shutdown requested")
        if self.vtk_pipeline.temp_file_path and os.path.exists(self.vtk_pipeline.temp_file_path):
            try:
                os.unlink(self.vtk_pipeline.temp_file_path)
            except Exception:
                pass
        self.data_loader.clear_cache()
        sys.exit(0)

Last the UI Component

"""UI component creation for the VTU Player interface."""

from trame.widgets import vuetify3 as vuetify, vtk, trame, iframe, html
from trame.ui.vuetify3 import SinglePageWithDrawerLayout

from config.settings import AppConfig
from visualization.enums import RenderMode, ColorMap

from loguru import logger

class UIComponents:
    """Creates and manages UI components for the VTU Player."""

    def __init__(self, server, vtk_pipeline, config: AppConfig):
        self.server = server
        self.state = server.state
        self.ctrl = server.controller
        self.vtk_pipeline = vtk_pipeline
        self.config = config
        self.iframe_message_handler = None

    def create_layout(self, iframe_message_handler=None):
        """Create the main application layout with iframe communication."""
        self.iframe_message_handler = iframe_message_handler
        
        with SinglePageWithDrawerLayout(self.server) as layout:
            layout.title.set_text("πŸ”¬ VTU Player")

            with layout.drawer as drawer:
                drawer.width = self.config.drawer_width
                self._create_drawer_content()

            with layout.content:
                self._create_main_content()

    def _create_main_content(self):
        """Create the main 3D viewport content."""
        with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
            view = vtk.VtkLocalView(self.vtk_pipeline.render_window)
            self.ctrl.view_update = view.update
            self.ctrl.view_reset_camera = view.reset_camera

    def _add_communication_components(self, message_handler):
        """Add iframe communication components to the layout."""
        # Create communicator for iframe events
        comm = iframe.Communicator(
            event_names=["parent_to_child"],
            parent_to_child=(message_handler, "[$event]"),
        )
        self.ctrl.child_post_message = comm.post_message
        logger.info('βœ… Iframe communicator created and attached to controller')

    def _create_drawer_content(self):
        """Create the drawer sidebar content."""
        vuetify.VDivider(classes="mt-4")

        # File selection
        vuetify.VSelect(
            v_model=("selected_file", ""),
            items=("available_files", []),
            label="VTU File",
            density="compact",
            variant="outlined",
            hide_details=True,
        )

        vuetify.VDivider(classes="mb-2")
        self._create_pipeline_widget()
        vuetify.VDivider(classes="mb-2")
        self._create_mesh_card()
        self._create_contour_card()

    def _create_pipeline_widget(self):
        """Create the pipeline visibility tree widget."""

        def actives_change(ids):
            active_id = ids[0] if ids else None
            if active_id == "1":
                self.state.active_ui = "mesh"
            elif active_id == "2":
                self.state.active_ui = "contour"
            else:
                self.state.active_ui = "nothing"

        def visibility_change(event):
            _id = event.get("id")
            visible = event.get("visible", True)
            if _id == "1" and self.vtk_pipeline.mesh_actor:
                self.vtk_pipeline.mesh_actor.SetVisibility(visible)
            elif _id == "2" and self.vtk_pipeline.contour_actor:
                self.vtk_pipeline.contour_actor.SetVisibility(visible)
            self.ctrl.view_update()

        trame.GitTree(
            sources=(
                "pipeline",
                [
                    {"id": "1", "parent": "0", "visible": 1, "name": "πŸ”· Mesh"},
                    {"id": "2", "parent": "1", "visible": 1, "name": "πŸ“ Contour"},
                ],
            ),
            actives_change=(actives_change, "[$event]"),
            visibility_change=(visibility_change, "[$event]"),
        )

    def _create_ui_card(self, title: str, ui_name: str):
        """Create a collapsible UI card."""
        with vuetify.VCard(v_show=f"active_ui == '{ui_name}'"):
            vuetify.VCardTitle(
                title,
                classes="grey lighten-1 py-1 grey--text text--darken-3",
                style="user-select: none; cursor: pointer",
                hide_details=True,
                dense=True,
            )
            return vuetify.VCardText(classes="py-2")

    def _create_mesh_card(self):
        """Create the mesh configuration card."""
        with self._create_ui_card("πŸ”· Mesh Configuration", "mesh"):
            # Representation selection
            vuetify.VSelect(
                v_model=("mesh_representation", RenderMode.SURFACE),
                items=("representations", self._get_representations()),
                label="Representation",
                prepend_icon="mdi-shape",
                hide_details=True,
                density="compact",
                variant="outlined",
                classes="pt-1",
            )

            # Color mapping controls
            with vuetify.VRow(classes="pt-2", dense=True):
                with vuetify.VCol(cols="6"):
                    vuetify.VSelect(
                        label="Color by",
                        v_model=("mesh_color_array_idx", 0),
                        items=("array_list", []),
                        prepend_icon="mdi-palette",
                        hide_details=True,
                        density="compact",
                        variant="outlined",
                        classes="pt-1",
                    )
                with vuetify.VCol(cols="6"):
                    vuetify.VSelect(
                        label="Colormap",
                        v_model=("mesh_color_preset", ColorMap.RAINBOW),
                        items=("colormaps", self._get_colormaps()),
                        prepend_icon="mdi-format-color-fill",
                        hide_details=True,
                        density="compact",
                        variant="outlined",
                        classes="pt-1",
                    )

            # Opacity control
            vuetify.VSlider(
                v_model=("mesh_opacity", self.config.default_opacity),
                min=0,
                max=1,
                step=0.1,
                label="Opacity",
                prepend_icon="mdi-opacity",
                classes="mt-1",
                hide_details=True,
                density="compact",
            )

    def _create_contour_card(self):
        """Create the contour configuration card."""
        with self._create_ui_card("πŸ“ Contour Configuration", "contour"):
            # Contour array selection
            vuetify.VSelect(
                label="Contour by",
                v_model=("contour_by_array_idx", 0),
                items=("array_list", []),
                prepend_icon="mdi-chart-line",
                hide_details=True,
                density="compact",
                variant="outlined",
                classes="pt-1",
            )

            # Contour value slider
            vuetify.VSlider(
                v_model=("contour_value", 0.5),
                min=("contour_min", 0.0),
                max=("contour_max", 1.0),
                step=("contour_step", 0.01),
                label="Contour Value",
                prepend_icon="mdi-tune",
                classes="my-1",
                hide_details=True,
                density="compact",
            )

            # Representation selection
            vuetify.VSelect(
                v_model=("contour_representation", RenderMode.SURFACE),
                items=("representations", self._get_representations()),
                label="Representation",
                prepend_icon="mdi-shape",
                hide_details=True,
                density="compact",
                variant="outlined",
                classes="pt-1",
            )

            # Color mapping controls
            with vuetify.VRow(classes="pt-2", dense=True):
                with vuetify.VCol(cols="6"):
                    vuetify.VSelect(
                        label="Color by",
                        v_model=("contour_color_array_idx", 0),
                        items=("array_list", []),
                        prepend_icon="mdi-palette",
                        hide_details=True,
                        density="compact",
                        variant="outlined",
                        classes="pt-1",
                    )
                with vuetify.VCol(cols="6"):
                    vuetify.VSelect(
                        label="Colormap",
                        v_model=("contour_color_preset", ColorMap.RAINBOW),
                        items=("colormaps", self._get_colormaps()),
                        prepend_icon="mdi-format-color-fill",
                        hide_details=True,
                        density="compact",
                        variant="outlined",
                        classes="pt-1",
                    )

            # Opacity control
            vuetify.VSlider(
                v_model=("contour_opacity", self.config.default_opacity),
                min=0,
                max=1,
                step=0.1,
                label="Opacity",
                prepend_icon="mdi-opacity",
                classes="mt-1",
                hide_details=True,
                density="compact",
            )

    def _get_representations(self):
        """Get available representation modes."""
        return [
            {"title": "Points", "value": RenderMode.POINTS},
            {"title": "Wireframe", "value": RenderMode.WIREFRAME},
            {"title": "Surface", "value": RenderMode.SURFACE},
            {"title": "Surface + Edges", "value": RenderMode.SURFACE_WITH_EDGES},
        ]

    def _get_colormaps(self):
        """Get available color maps."""
        return [
            {"title": "🌈 Rainbow", "value": ColorMap.RAINBOW},
            {"title": "πŸ”„ Inv Rainbow", "value": ColorMap.INVERTED_RAINBOW},
            {"title": "⬜ Greyscale", "value": ColorMap.GREYSCALE},
            {"title": "⬛ Inv Greyscale", "value": ColorMap.INVERTED_GREYSCALE},
        ]

    def setup_state_defaults(self):
        """Setup default state values for UI components."""
        # This method can be called to ensure UI state is properly initialized
        if not hasattr(self.state, "representations"):
            self.state.representations = self._get_representations()

        if not hasattr(self.state, "colormaps"):
            self.state.colormaps = self._get_colormaps()

        # Ensure array_list is initialized
        if not hasattr(self.state, "array_list"):
            self.state.array_list = []

It's seems that there is no communication the two project. The vue app is on localhost:3000 while the trame app is on localhost:3015. Is there any solution? Thanks in advice

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions