Skip to content

c2f4dt.ui.import_summary_dialog

c2f4dt.ui.import_summary_dialog

ImportSummaryDialog

Bases: QDialog

Modal dialog to review imported objects and choose import options.

Inspired by CloudCompare. For each object we show discovered attributes (read-only) and let the user: - Remap axes with sign (e.g., X=+Y, Y=-Z, Z=+X) - Apply same mapping to normals - Compute normals if missing - Choose coloring preference (RGB or colormap over fake intensity)

This dialog DOES NOT mutate input objects. Use operations() to retrieve what the user selected and apply it in the caller.

Source code in src/c2f4dt/ui/import_summary_dialog.py
class ImportSummaryDialog(QtWidgets.QDialog):
    """Modal dialog to review imported objects and choose import options.

    Inspired by CloudCompare. For each object we show discovered attributes
    (read-only) and let the user:
      - Remap axes with sign (e.g., X=+Y, Y=-Z, Z=+X)
      - Apply same mapping to normals
      - Compute normals if missing
      - Choose coloring preference (RGB or colormap over fake intensity)

    This dialog DOES NOT mutate input objects. Use `operations()` to
    retrieve what the user selected and apply it in the caller.
    """

    def __init__(self, objects: Iterable[ImportedObject], parent=None) -> None:
        super().__init__(parent)
        self.setWindowTitle("Import summary")
        self.setModal(True)
        self._objects: List[ImportedObject] = list(objects)

        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(8, 8, 8, 8)
        layout.setSpacing(8)

        # One collapsible-like page per object (CloudCompare-like feel)
        self.toolbox = QtWidgets.QToolBox(self)
        layout.addWidget(self.toolbox, 1)

        self._pages: List[Dict[str, Any]] = []
        for obj in self._objects:
            page = self._build_page(obj)
            self.toolbox.addItem(page["widget"], obj.name)
            self._pages.append(page)

        # Bounds/meta hint
        self.lblMeta = QtWidgets.QLabel("")
        self.lblMeta.setWordWrap(True)
        layout.addWidget(self.lblMeta)
        self._populate_bounds()

        btns = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
        )
        btns.accepted.connect(self.accept)
        btns.rejected.connect(self.reject)
        layout.addWidget(btns)

    # ------------------------------------------------------------------
    # UI builders
    # ------------------------------------------------------------------
    def _build_page(self, obj: ImportedObject) -> Dict[str, Any]:
        """Create one page with labels and options for an object.

        Args:
            obj: ImportedObject to present.

        Returns:
            A dict with references to created widgets to read them later.
        """
        w = QtWidgets.QWidget()
        form = QtWidgets.QFormLayout(w)
        form.setLabelAlignment(QtCore.Qt.AlignRight)
        form.setFormAlignment(QtCore.Qt.AlignTop)

        # ---- Read-only attributes
        npts = obj.points.shape[0] if obj.points is not None else (
            obj.pv_mesh.n_points if obj.pv_mesh is not None else 0
        )
        nfaces = obj.faces.shape[0] if obj.faces is not None else (
            obj.pv_mesh.n_faces if obj.pv_mesh is not None else 0
        )
        has_rgb = obj.colors is not None
        has_int = obj.intensity is not None
        has_nrm = getattr(obj, "normals", None) is not None

        form.addRow("Kind", QtWidgets.QLabel(obj.kind))
        form.addRow("#Points", QtWidgets.QLabel(str(npts)))
        form.addRow("#Faces", QtWidgets.QLabel(str(nfaces)))
        form.addRow("Has RGB", QtWidgets.QLabel("Yes" if has_rgb else "No"))
        form.addRow("Has Intensity", QtWidgets.QLabel("Yes" if has_int else "No"))
        form.addRow("Has Normals", QtWidgets.QLabel("Yes" if has_nrm else "No"))

        # ---- Axis mapping (with presets)
        axis_opts = ["+X", "-X", "+Y", "-Y", "+Z", "-Z"]
        cmb_preset = QtWidgets.QComboBox()
        cmb_preset.addItems([
            "Custom",
            "Z-up (identity)",      # X->+X, Y->+Y, Z->+Z
            "Y-up (swap Y/Z)",      # X->+X, Y->+Z, Z->-Y
            "X-up (swap X/Z)",      # X->+Z, Y->+Y, Z->-X
            "Flip Z",               # X->+X, Y->+Y, Z->-Z
            "Flip Y",               # X->+X, Y->-Y, Z->+Z
            "Flip X",               # X->-X, Y->+Y, Z->+Z
        ])

        cmb_x = QtWidgets.QComboBox(); cmb_x.addItems(axis_opts); cmb_x.setCurrentText("+X")
        cmb_y = QtWidgets.QComboBox(); cmb_y.addItems(axis_opts); cmb_y.setCurrentText("+Y")
        cmb_z = QtWidgets.QComboBox(); cmb_z.addItems(axis_opts); cmb_z.setCurrentText("+Z")

        def apply_preset(name: str) -> None:
            presets = {
                "Z-up (identity)": ("+X", "+Y", "+Z"),
                "Y-up (swap Y/Z)": ("+X", "+Z", "-Y"),
                "X-up (swap X/Z)": ("+Z", "+Y", "-X"),
                "Flip Z": ("+X", "+Y", "-Z"),
                "Flip Y": ("+X", "-Y", "+Z"),
                "Flip X": ("-X", "+Y", "+Z"),
            }
            if name in presets:
                x, y, z = presets[name]
                cmb_x.setCurrentText(x)
                cmb_y.setCurrentText(y)
                cmb_z.setCurrentText(z)

        cmb_preset.currentTextChanged.connect(apply_preset)

        form.addRow("Axis preset", cmb_preset)
        form.addRow("Map to X", cmb_x)
        form.addRow("Map to Y", cmb_y)
        form.addRow("Map to Z", cmb_z)

        chk_apply_normals = QtWidgets.QCheckBox("Apply same mapping to normals (if present)")
        chk_apply_normals.setChecked(True)
        form.addRow("Normals mapping", chk_apply_normals)

        chk_compute_normals = QtWidgets.QCheckBox("Compute normals if missing")
        chk_compute_normals.setChecked(not has_nrm)
        form.addRow("Normals compute", chk_compute_normals)

        # ---- Coloring preference
        grp_color = QtWidgets.QGroupBox("Coloring preference")
        v = QtWidgets.QVBoxLayout(grp_color)
        rad_rgb = QtWidgets.QRadioButton("Use RGB (if available)")
        rad_cmap = QtWidgets.QRadioButton("Use colormap (fake intensity)")
        if has_rgb:
            rad_rgb.setChecked(True)
        else:
            rad_cmap.setChecked(True)
        v.addWidget(rad_rgb)
        v.addWidget(rad_cmap)
        form.addRow(grp_color)

        return {
            "widget": w,
            "cmb_preset": cmb_preset,
            "cmb_x": cmb_x,
            "cmb_y": cmb_y,
            "cmb_z": cmb_z,
            "chk_apply_normals": chk_apply_normals,
            "chk_compute_normals": chk_compute_normals,
            "rad_rgb": rad_rgb,
            "rad_cmap": rad_cmap,
            "obj": obj,
        }

    def _populate_bounds(self) -> None:
        meta_desc = []
        for obj in self._objects:
            b = obj.bounds() if hasattr(obj, "bounds") else None
            if b:
                meta_desc.append(
                    f"{obj.name} bounds: x=[{b[0]:.3f},{b[1]:.3f}] "
                    f"y=[{b[2]:.3f},{b[3]:.3f}] z=[{b[4]:.3f},{b[5]:.3f}]"
                )
        self.lblMeta.setText("\n".join(meta_desc))

    # ------------------------------------------------------------------
    # Results
    # ------------------------------------------------------------------
    @property
    def objects(self) -> List[ImportedObject]:
        """Return original objects (unchanged)."""
        return self._objects

    def operations(self) -> List[Dict[str, Any]]:
        """Return per-object operations selected in the dialog.

        Each entry:
          - axis_map: {'X': str, 'Y': str, 'Z': str} in {+X,-X,+Y,-Y,+Z,-Z}
          - map_normals: bool
          - compute_normals_if_missing: bool
          - color_preference: 'rgb' | 'colormap'
        """
        ops: List[Dict[str, Any]] = []
        for page in self._pages:
            axis_map = {
                "X": page["cmb_x"].currentText(),
                "Y": page["cmb_y"].currentText(),
                "Z": page["cmb_z"].currentText(),
            }
            color_pref = "rgb" if page["rad_rgb"].isChecked() else "colormap"
            ops.append(
                {
                    "axis_map": axis_map,
                    "map_normals": page["chk_apply_normals"].isChecked(),
                    "compute_normals_if_missing": page["chk_compute_normals"].isChecked(),
                    "color_preference": color_pref,
                }
            )
        return ops

lblMeta = QtWidgets.QLabel('') instance-attribute

objects property

Return original objects (unchanged).

toolbox = QtWidgets.QToolBox(self) instance-attribute

operations()

Return per-object operations selected in the dialog.

Each entry
  • axis_map: {'X': str, 'Y': str, 'Z': str} in {+X,-X,+Y,-Y,+Z,-Z}
  • map_normals: bool
  • compute_normals_if_missing: bool
  • color_preference: 'rgb' | 'colormap'
Source code in src/c2f4dt/ui/import_summary_dialog.py
def operations(self) -> List[Dict[str, Any]]:
    """Return per-object operations selected in the dialog.

    Each entry:
      - axis_map: {'X': str, 'Y': str, 'Z': str} in {+X,-X,+Y,-Y,+Z,-Z}
      - map_normals: bool
      - compute_normals_if_missing: bool
      - color_preference: 'rgb' | 'colormap'
    """
    ops: List[Dict[str, Any]] = []
    for page in self._pages:
        axis_map = {
            "X": page["cmb_x"].currentText(),
            "Y": page["cmb_y"].currentText(),
            "Z": page["cmb_z"].currentText(),
        }
        color_pref = "rgb" if page["rad_rgb"].isChecked() else "colormap"
        ops.append(
            {
                "axis_map": axis_map,
                "map_normals": page["chk_apply_normals"].isChecked(),
                "compute_normals_if_missing": page["chk_compute_normals"].isChecked(),
                "color_preference": color_pref,
            }
        )
    return ops