DetailView changes with grasshopper

I’m trying to programmatically modify an existing DetailView in a Rhino 8 layout using RhinoCommon (.NET 7). The code successfully finds the target detail, updates its camera location, target, projection, and display mode, and then calls CommitViewportChanges() followed by CommitChanges(). However, none of these visual changes appear in the layout — the detail’s view remains unchanged even though no errors are reported. It seems like the viewport updates aren’t being reflected or synchronized correctly between the DetailViewObject, its runtime DetailView, and the layout page.

// -*- coding: utf-8 -*-
// ORIOL — Components/Layout/DetailEdit
// Author: Sebastián Valenzuela Ferry
// Description: Modifies an existing Detail by applying camera, projection, display mode, 
// and frustum from a reference view, with proper commit order for Rhino 8 / .NET 7.

using Grasshopper.Kernel;
using Oriol.Core;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using System;
using System.Drawing;
using System.Linq;
using DisplayModeDescription = Rhino.Display.DisplayModeDescription;
using ViewportInfo = Rhino.DocObjects.ViewportInfo;

namespace Oriol.Components.Layout
{
    public class DetailEdit : GH_Component
    {
        private bool _prevRun = false;

        public DetailEdit()
            : base("Edit Detail", "DetailEdit",
                   "Edits an existing Detail applying camera, projection, display mode, and frustum from a reference view.",
                   "ORIOL", "LAYOUT")
        { }

        public override GH_Exposure Exposure => GH_Exposure.primary;

        protected override void RegisterInputParams(GH_InputParamManager p)
        {
            p.AddBooleanParameter("Run", "R", "Executes the action (toggle or button).", GH_ParamAccess.item, false);
            p.AddTextParameter("GUID", "G", "GUID of the DetailView object to edit.", GH_ParamAccess.item);
            p.AddTextParameter("Display", "D", "Display mode (Wireframe, Rendered, etc.).", GH_ParamAccess.item);
            p.AddBoxParameter("Target", "T", "Target box or camera focus point.", GH_ParamAccess.item);
            p.AddNumberParameter("Scale", "S", "Page-to-model scale.", GH_ParamAccess.item, 1.0);
            p.AddIntegerParameter("Projection", "P", "Projection type (Top, Front, Perspective...).", GH_ParamAccess.item, 0);
            p.AddGenericParameter("View", "V", "View or ViewportInfo to replicate camera and frustum from.", GH_ParamAccess.item);
            for (int i = 2; i <= 6; i++) p[i].Optional = true;
        }

        protected override void RegisterOutputParams(GH_OutputParamManager p)
        {
            p.AddTextParameter("Result", "R", "Operation result.", GH_ParamAccess.item);
            p.AddTextParameter("Display", "D", "Final display mode.", GH_ParamAccess.item);
            p.AddPointParameter("Target", "T", "Resulting camera target.", GH_ParamAccess.item);
            p.AddNumberParameter("Scale", "S", "Final detail scale.", GH_ParamAccess.item);
            p.AddTextParameter("Projection", "P", "Current projection name.", GH_ParamAccess.item);
            p.AddPointParameter("Camera", "C", "Resulting camera location (for debugging).", GH_ParamAccess.item);
        }

        protected override void SolveInstance(IGH_DataAccess DA)
        {
            bool run = false;
            string guidText = "";
            string displayName = "";
            BoundingBox targetBox = BoundingBox.Empty;
            double scale = 1.0;
            int projInt = 0;
            object viewInput = null!;

            DA.GetData(0, ref run);
            DA.GetData(1, ref guidText);
            DA.GetData(2, ref displayName);
            DA.GetData(3, ref targetBox);
            DA.GetData(4, ref scale);
            DA.GetData(5, ref projInt);
            DA.GetData(6, ref viewInput);

            // Rising edge detection
            bool risingEdge = run && !_prevRun;
            _prevRun = run;
            if (!risingEdge) return;

            // === Basic validation ===
            if (!Guid.TryParse(guidText, out Guid detailId))
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Invalid GUID.");
                return;
            }

            var doc = RhinoDoc.ActiveDoc;
            if (doc == null)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "No active document.");
                return;
            }

            var detailObj = doc.Objects.FindId(detailId) as DetailViewObject;
            if (detailObj == null)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Detail with this GUID not found.");
                return;
            }

            // Find the PageView and runtime DetailView
            var page = doc.Views.GetPageViews()
                .FirstOrDefault(pv => pv.GetDetailViews().Any(dv => dv.Id == detailObj.Attributes.Id));
            if (page == null)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "The Detail does not belong to any active Layout.");
                return;
            }

            var runtimeDetail = page.GetDetailViews().FirstOrDefault(dv => dv.Id == detailObj.Attributes.Id);
            if (runtimeDetail == null)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "No active DetailView found on this page.");
                return;
            }

            var vp = runtimeDetail.Viewport;
            if (vp == null)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Could not get Viewport from Detail.");
                return;
            }

            // Activate layout and its detail
            page.SetPageAsActive();
            doc.Views.ActiveView = page;
            page.SetActiveDetail(runtimeDetail.Id);
            RhinoApp.Wait();

            var displayMode = DisplayModeDescription.GetDisplayModes()
                .FirstOrDefault(m => m.DisplayAttributes.EnglishName.Equals(displayName, StringComparison.OrdinalIgnoreCase))
                ?? vp.DisplayMode;

            var projection = Enum.IsDefined(typeof(Rhino.Display.DefinedViewportProjection), projInt)
                ? (Rhino.Display.DefinedViewportProjection)projInt
                : Rhino.Display.DefinedViewportProjection.None;

            bool prevRedraw = doc.Views.RedrawEnabled;
            doc.Views.RedrawEnabled = false;

            try
            {
                // === 1 Base camera or reference view ===
                if (viewInput == null)
                {
                    if (targetBox.IsValid)
                        vp.SetCameraTarget(targetBox.Center, true);

                    if (projection != Rhino.Display.DefinedViewportProjection.None)
                        vp.SetProjection(projection, projection.ToString(), true);
                }
                else
                {
                    var srcVp = ViewInterpreter.ResolveViewport(viewInput);
                    if (srcVp != null)
                    {
                        bool applied = ViewInterpreter.ApplyToDetail(srcVp, detailObj, true);
                        RhinoApp.WriteLine(applied
                            ? "[ORIOL] Frustum and camera replicated from reference view."
                            : "[ORIOL] Frustum replication failed.");
                    }
                    else
                    {
                        AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Reference view could not be resolved.");
                    }
                }

                // === 2 Display mode and target ===
                if (!string.IsNullOrWhiteSpace(displayName))
                    vp.DisplayMode = displayMode;

                if (targetBox.IsValid)
                {
                    vp.SetCameraTarget(targetBox.Center, true);
                    if (targetBox.Diagonal.Length > 0)
                        vp.ZoomBoundingBox(targetBox);
                }

                //  FIRST commit viewport
                runtimeDetail.CommitViewportChanges();

                // === 3 Persistent scale (DetailGeometry) ===
                if (vp.IsParallelProjection && scale > 0 &&
                    Math.Abs(scale - detailObj.DetailGeometry.PageToModelRatio) > RhinoMath.ZeroTolerance)
                {
                    bool prevLock = detailObj.DetailGeometry.IsProjectionLocked;
                    detailObj.DetailGeometry.IsProjectionLocked = false;
                    detailObj.DetailGeometry.SetScale(1, doc.ModelUnitSystem, scale, doc.PageUnitSystem);
                    detailObj.DetailGeometry.IsProjectionLocked = prevLock;
                }

                // Synchronize persistence and save
                detailObj.DetailGeometry.Viewport = new ViewportInfo(vp);
                detailObj.CommitChanges();
            }
            catch (Exception ex)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Error: {ex.Message}");
                DA.SetData(0, "Failed");
                doc.Views.RedrawEnabled = prevRedraw;
                return;
            }
            finally
            {
                doc.Views.RedrawEnabled = prevRedraw;
            }

            // === 4 Final Redraw ===
            page.Redraw();
            doc.Views.ActiveView = page;
            doc.Views.Redraw();
            RhinoApp.Wait();

            RhinoApp.WriteLine($"[ORIOL] DetailEdit OK → Mode:{vp.DisplayMode.EnglishName} | Target:{vp.CameraTarget}");

            // --- Outputs ---
            DA.SetData(0, "Success");
            DA.SetData(1, vp.DisplayMode.EnglishName);
            DA.SetData(2, vp.CameraTarget);
            DA.SetData(3, detailObj.DetailGeometry.PageToModelRatio);
            DA.SetData(4, vp.Name);
            DA.SetData(5, vp.CameraLocation);
        }

        protected override Bitmap Icon =>
            Rhino.UI.DrawingUtilities.BitmapFromIconResource("Oriol.Resources.Layout.DetailEdit.png", typeof(DetailEdit).Assembly);

        public override Guid ComponentGuid =>
            new Guid("FA2D07D3-B77A-4BAF-BEAD-AB61E2C1207E");
    }
}

Any help or insight would be greatly appreciated.
Thanks in advance to anyone who can help me figure out why the DetailView camera changes are not taking effect in Rhino 8.

PD: The view that looks green is the one you should see in detailview.

There’s a lot going on in your code, and I have to admit I did not try to run it, but did you try to commit your changes through CommitViewportChanges ?

// -*- coding: utf-8 -*-
// ORIOL — Components/Layout/NewDetail (Rhino 8 / .NET 7)
// Autor: Sebastián Valenzuela Ferry
// Descripción:
//   Crea uno o varios DetailView en Layouts existentes basados en la vista activa.
//   Copia cámara, proyección y modo de visualización desde la vista fuente,
//   pero si se proporciona entrada en “Display Mode”, esa tiene prioridad.
//   Incluye persistencia visual, bloqueo de redibujado tipo “IterStar” y log de rendimiento.

#nullable enable

using Grasshopper.Kernel;
using Rhino;
using Rhino.Display;
using Rhino.DocObjects;
using Rhino.Geometry;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;

namespace Oriol.Components.Layout
{
    public class NewDetail : GH_Component
    {
        private bool _lastRun = false;

        private static readonly Dictionary<Guid, (string, List<Guid>, List<string>)> _lastOutputs = new();
        private static readonly Dictionary<Guid, string> _displayHistory = new();

        public NewDetail() : base(
            "New Detail", "NewDetail",
            "Crea uno o varios DetailView en Layouts existentes de Rhino 8. Copia cámara, proyección y modo de visualización desde la vista de referencia, o usa el modo conectado en “Display Mode”.",
            "Oriol", "Layout")
        { }

        public override Guid ComponentGuid => new("XXXXXXXXXXXXXX");

#pragma warning disable CA1416
        protected override Bitmap Icon
        {
            get
            {
                var asm = typeof(NewDetail).Assembly;
                using var s = asm.GetManifestResourceStream("Oriol.Resources.NewDetail.png");
                return s != null ? new Bitmap(s) : new Bitmap(24, 24);
            }
        }
#pragma warning restore CA1416

        public override GH_Exposure Exposure => GH_Exposure.primary;

        protected override void RegisterInputParams(GH_InputParamManager p)
        {
            p.AddBooleanParameter("Run", "R", "Ejecutar creación (flanco ascendente)", GH_ParamAccess.item, false);
            p.AddTextParameter("Layout", "L", "Nombre del Layout de destino", GH_ParamAccess.list);
            p.AddCurveParameter("Bounds", "B", "Curvas cerradas (rectángulos) en coordenadas de página", GH_ParamAccess.list);
            p.AddTextParameter("Nombre", "N", "Nombre visible del Detail", GH_ParamAccess.list, "DetailView");
            p.AddNumberParameter("Escala", "S", "Escala (1:X) — 1.0 = 1:1", GH_ParamAccess.list, 50.0);
            p.AddGenericParameter("View Source", "V", "Vista de referencia (nombre, NamedView, RhinoView o Gh_View)", GH_ParamAccess.list);
            p.AddTextParameter("Display Mode", "M", "Modo de visualización opcional (Wireframe, Shaded, Rendered, Ghosted, Arctic, etc.)", GH_ParamAccess.list);
            p[6].Optional = true;
        }

        protected override void RegisterOutputParams(GH_OutputParamManager p)
        {
            p.AddTextParameter("Status", "S", "Mensajes de estado del proceso.", GH_ParamAccess.list);
            p.AddGenericParameter("Detail Id", "ID", "Identificadores GUID de los Details creados.", GH_ParamAccess.list);
            p.AddTextParameter("Detail Name", "DN", "Nombres de los Details creados.", GH_ParamAccess.list);
        }

        protected override void SolveInstance(IGH_DataAccess DA)
        {
            bool run = false;
            if (!DA.GetData(0, ref run)) return;

            bool risingEdge = run && !_lastRun;
            _lastRun = run;

            if (!risingEdge)
            {
                if (_lastOutputs.TryGetValue(InstanceGuid, out var cached))
                {
                    DA.SetDataList(0, new List<string> { cached.Item1 });
                    DA.SetDataList(1, cached.Item2);
                    DA.SetDataList(2, cached.Item3);
                }
                else
                    DA.SetData(0, "🟡 Inactivo — sin datos previos.");
                return;
            }

            var layoutName = new List<string>();
            var bounds = new List<Curve>();
            var nombre = new List<string>();
            var scale = new List<double>();
            var views = new List<object>();
            var displayModeName = new List<string>();

            DA.GetDataList(1, layoutName);
            DA.GetDataList(2, bounds);
            DA.GetDataList(3, nombre);
            DA.GetDataList(4, scale);
            DA.GetDataList(5, views);
            DA.GetDataList(6, displayModeName);

            if (layoutName.Count == 0 || bounds.Count == 0)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Entradas insuficientes (Layout o Bounds).");
                return;
            }

            int count = new[] { layoutName.Count, bounds.Count, nombre.Count, scale.Count, views.Count }.Max();

            var statusList = new List<string>();
            var idList = new List<Guid>();
            var nameList = new List<string>();
            int createdCount = 0, failedCount = 0;

            var doc = RhinoDoc.ActiveDoc;
            if (doc == null)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "No hay documento activo.");
                return;
            }

            uint undo = doc.BeginUndoRecord("ORIOL.NewDetail.Batch");
            bool prevRedraw = doc.Views.RedrawEnabled;
            doc.Views.RedrawEnabled = false;
            var timer = Stopwatch.StartNew();

            try
            {
                for (int i = 0; i < count; i++)
                {
                    string? layoutName_i = SafeIndex(layoutName, i) ?? layoutName.LastOrDefault();
                    Curve? bounds_i = SafeIndex(bounds, i);
                    string nombre_i = SafeIndex(nombre, i, $"Detail_{i}") ?? $"Detail_{i}";
                    double scale_i = SafeIndex(scale, i, 50.0);
                    object? viewInput_i = SafeIndex(views, i);
                    string? displayMode_i = SafeIndex(displayModeName, i);

                    var page = doc.Views.GetPageViews()
                        .FirstOrDefault(v => v.PageName.Equals(layoutName_i, StringComparison.OrdinalIgnoreCase));
                    if (page == null)
                    {
                        statusList.Add($"⚠️ Layout '{layoutName_i}' no encontrado.");
                        idList.Add(Guid.Empty);
                        nameList.Add(nombre_i);
                        failedCount++;
                        continue;
                    }

                    doc.Views.ActiveView = page;

                    if (bounds_i == null || !bounds_i.IsValid || !bounds_i.TryGetPlane(out Plane plano))
                    {
                        statusList.Add($"⚠️ Curva inválida para '{nombre_i}'.");
                        idList.Add(Guid.Empty);
                        nameList.Add(nombre_i);
                        failedCount++;
                        continue;
                    }

                    var bb = bounds_i.GetBoundingBox(true);
                    var rect = new Rectangle3d(plano, bb.Min, bb.Max);
                    Point2d corner0 = new(rect.Corner(3).X, rect.Corner(3).Y);
                    Point2d corner1 = new(rect.Corner(1).X, rect.Corner(1).Y);

                    var existing = page.GetDetailViews()
                        .FirstOrDefault(d => d.Attributes.Name == nombre_i || d.Name == nombre_i);
                    if (existing != null)
                        doc.Objects.Delete(existing.Id, true);

                    var srcInfo = Oriol.Core.ViewInterpreter.ResolveViewportInfo(viewInput_i!);
                    if (srcInfo is null)
                    {
                        var fallbackView = doc.Views.ActiveView
                            ?? doc.Views.GetViewList(ViewTypeFilter.ModelStyleViews).FirstOrDefault();
                        if (fallbackView != null)
                            srcInfo = new ViewportInfo(fallbackView.ActiveViewport);
                        else
                        {
                            statusList.Add($"⚠️ Vista no válida para '{nombre_i}'.");
                            idList.Add(Guid.Empty);
                            nameList.Add(nombre_i);
                            failedCount++;
                            continue;
                        }
                    }

                    var projection = srcInfo.IsParallelProjection
                        ? DefinedViewportProjection.Top
                        : DefinedViewportProjection.Perspective;

                    var detailObj = page.AddDetailView(nombre_i, corner0, corner1, projection);
                    if (detailObj == null)
                    {
                        statusList.Add($"❌ No se pudo crear '{nombre_i}' en '{layoutName_i}'.");
                        idList.Add(Guid.Empty);
                        nameList.Add(nombre_i);
                        failedCount++;
                        continue;
                    }

                    detailObj.Attributes.Name = nombre_i;
                    detailObj.Attributes.SetUserString("ORIOL.DetailName", nombre_i);
                    detailObj.CommitChanges();

                    // 🔹 Aplicar cámara y DisplayMode de la vista fuente
                    Oriol.Core.ViewInterpreter.ApplyViewportInfoToDetail(srcInfo, detailObj.Viewport);

                    // 🔹 Si se da entrada "Display Mode", sobrescribir el modo heredado
                    string? finalMode = displayMode_i;
                    if (string.IsNullOrWhiteSpace(finalMode))
                    {
                        // Si no hay entrada, intentar usar modo previo persistente
                        _displayHistory.TryGetValue(detailObj.Id, out finalMode);
                    }

                    if (!string.IsNullOrWhiteSpace(finalMode))
                    {
                        var mode = DisplayModeDescription.GetDisplayModes()
                            .FirstOrDefault(m => m.EnglishName.Equals(finalMode, StringComparison.OrdinalIgnoreCase));
                        if (mode != null)
                        {
                            detailObj.Viewport.DisplayMode = mode;

                            // 🧭 Rhino 8: CommitViewportChanges() refresca el pipeline visual
                            detailObj.CommitViewportChanges();
                            page.Redraw();

                            _displayHistory[detailObj.Id] = mode.EnglishName;
                        }
                        else
                        {
                            AddRuntimeMessage(GH_RuntimeMessageLevel.Warning,
                                $"Modo '{finalMode}' no encontrado, se mantiene el de la vista fuente.");
                        }
                    }






                    // Escala (heredada o de entrada)
                    if (viewInput_i is DetailViewObject srcDetail)
                    {
                        double inheritedScale = srcDetail.DetailGeometry.PageToModelRatio;
                        if (inheritedScale > 0)
                            scale_i = inheritedScale;
                    }

                    if (scale_i > 0)
                    {
                        var dgeom = detailObj.DetailGeometry;
                        dgeom.SetScale(1.0 / scale_i, doc.ModelUnitSystem, 1.0, doc.PageUnitSystem);
                        dgeom.IsProjectionLocked = true;
                        detailObj.CommitChanges();
                    }

                    detailObj.CommitViewportChanges();
                    detailObj.CommitChanges();

                    Guid detailId = detailObj.Id;
                    if (detailId == Guid.Empty)
                    {
                        var found = doc.Objects.FindByObjectType(ObjectType.Detail)
                            .OfType<DetailViewObject>()
                            .FirstOrDefault(d => d.Attributes.Name == nombre_i);
                        if (found != null)
                            detailId = found.Id;
                    }

                    statusList.Add($"✅ Detail '{nombre_i}' creado correctamente en layout '{layoutName_i}'.");
                    idList.Add(detailId);
                    nameList.Add(nombre_i);
                    createdCount++;
                }
            }
            finally
            {
                timer.Stop();
                double seconds = timer.Elapsed.TotalSeconds;

                doc.EndUndoRecord(undo);
                doc.Views.RedrawEnabled = prevRedraw;
                doc.Views.Redraw();

                string emoji = failedCount > 0 ? "⚠️" : "✅";
                string report = $"{emoji} {createdCount} Details creados en {seconds:0.00} s ({failedCount} fallidos)";
                RhinoApp.WriteLine($"[ORIOL] {report}");
                statusList.Add(report);
            }

            _lastOutputs[InstanceGuid] = ($"{statusList.LastOrDefault()}", idList, nameList);

            DA.SetDataList(0, statusList);
            DA.SetDataList(1, idList);
            DA.SetDataList(2, nameList);
        }

        private static T? SafeIndex<T>(IReadOnlyList<T>? list, int index, T? fallback = default)
        {
            if (list is null || list.Count == 0)
                return fallback;
            return (index >= 0 && index < list.Count) ? list[index] : fallback;
        }
    }
}

1 Like