Eto forms, Cell color from Layer color?

I do have this basic script and I do want if possible to get the cells from the Material column to have the same color as the color of the layer on which each object is on. It is possible to achieve this? This script needs to work on both Rhino v7 for MacOS and Windows.

#!/usr/bin/env ironpython2.7
# -*- coding: utf8 -*-
# SimpleCutlist.py

import Rhino
import Rhino.Geometry as g
import rhinoscriptsyntax as rs
import Eto.Drawing as drawing
import Eto.Forms as forms
import scriptcontext

# List of layers to exclude
EXCLUDED_LAYERS = ["Booleans", "Dim", "Default", "Walls"]

# Attributes for the cutlist
class attr:
    def __init__(self, name, isEditable, valType):
        self.name = name
        self.isEditable = isEditable
        self.valType = valType

ATTRS = (
    attr("_N", True, str),        # N (number)
    attr("_Name", True, str),     # Name
    attr("Length", True, float),  # Length
    attr("Width", True, float),   # Width
    attr("Thickness", True, float), # Thickness
    attr("Material", True, str),  # Material
)

def computeDims(obj):
    objRef = Rhino.DocObjects.ObjRef(obj)
    brep = objRef.Geometry()
    edges = list(brep.Edges)
    edges.sort(key=lambda edge: edge.GetLength(), reverse=True)
    edge = edges[0]
    origin = edge.PointAtStart
    xPoint = edge.PointAtEnd
    xVec = Rhino.Geometry.Vector3d(xPoint - origin)
    xVec.Unitize()
    face = brep.Faces[0]
    yVec = Rhino.Geometry.Vector3d.CrossProduct(face.NormalAt(0, 0), xVec)
    yVec.Unitize()
    plane = Rhino.Geometry.Plane(origin, xVec, yVec)
    verts = list(brep.Vertices)
    tMatrix = g.Transform.ChangeBasis(g.Plane.WorldXY, plane)
    map(lambda vert: vert.Transform(tMatrix), verts)
    verts = list(map(lambda vert: vert.Location, verts))
    verts.sort(key=lambda vert: vert.X)
    length = verts[-1].X - verts[0].X
    verts.sort(key=lambda vert: vert.Y)
    width = verts[-1].Y - verts[0].Y
    verts.sort(key=lambda vert: vert.Z)
    thickness = verts[-1].Z - verts[0].Z
    return [length, width, thickness]

class Detail:
    def __init__(self, obj):
        self.obj = obj
        self.id = obj
        for attr_obj in ATTRS:
            key = attr_obj.name
            val = rs.GetUserText(str(obj), key)
            if val is None:
                default = "0" if attr_obj.valType in [float, str] and key != "Material" else ""
                setattr(self, key, default)
                rs.SetUserText(str(obj), key, default)
            else:
                setattr(self, key, val)
        self.setDims()
        self.update_material_from_layer()

    def setDims(self):
        dims = computeDims(self.obj)
        self.Length = str(round(dims[0], 2))
        self.Width = str(round(dims[1], 2))
        self.Thickness = str(round(dims[2], 2))
        rs.SetUserText(str(self.id), "Length", self.Length)
        rs.SetUserText(str(self.id), "Width", self.Width)
        rs.SetUserText(str(self.id), "Thickness", self.Thickness)

    def update_material_from_layer(self):
        objRef = Rhino.DocObjects.ObjRef(self.id)
        index = objRef.Object().Attributes.LayerIndex
        layer = scriptcontext.doc.Layers[index]
        self.Material = layer.Name
        self.layer_color = drawing.Color.FromArgb(layer.Color.ToArgb())
        rs.SetUserText(str(self.id), "Material", self.Material)

    def getParams(self):
        return [getattr(self, attr.name) for attr in ATTRS]

    def refresh(self):
        for attr in ATTRS:
            rs.SetUserText(str(self.id), attr.name, getattr(self, attr.name))

class Specification:
    def __init__(self, details):
        self.details = []
        idSet = set(range(len(details)))
        self.table = []
        while idSet:
            curId = idSet.pop()
            curDet = details[curId]
            curDetails = [curDet]
            for id in list(idSet):
                if curDet.getParams() == details[id].getParams():
                    curDetails.append(details[id])
                    idSet.remove(id)
            self.details.append(curDetails)
            self.table.append(curDet.getParams())

class SimpleCutlistDialog(forms.Dialog[bool]):
    def __init__(self):
        self.Title = "Simple Cutlist"
        self.Padding = drawing.Padding(2)
        self.Resizable = True
        self.m_gridview = forms.GridView()
        self.m_gridview.ShowHeader = True

        for i, attr in enumerate(ATTRS):
            column = forms.GridColumn()
            column.HeaderText = attr.name if attr.name[0] != "_" else attr.name[1:]
            column.Editable = attr.isEditable
            column.DataCell = forms.TextBoxCell(i)
            self.m_gridview.Columns.Add(column)

        self.button = forms.Button(Text="Export CSV")
        self.button.Width = 80  # Set a smaller, specific width
        self.button.Click += self.buttonClick

        layout = forms.DynamicLayout()
        layout.AddRow(self.m_gridview)
        layout.AddRow(self.button)
        self.Content = layout
        self.ClientSize = drawing.Size(600, 130)

    def buttonClick(self, sender, e):
        data = [[col.HeaderText for col in self.m_gridview.Columns]]
        data.extend(self.spec.table)
        filename = rs.SaveFileName("Save cutlist as", "CSV File (*.csv)|*.csv||")
        if filename:
            with open(filename, 'w') as f:
                for row in data:
                    f.write("|".join(str(item) for item in row) + "\n")

    def setData(self, spec):
        self.spec = spec
        self.m_gridview.DataStore = spec.table

def main():
    objs = rs.GetObjects("Select polysurface objects", 16, preselect=True)
    if not objs:
        return

    filtered_objs = [obj for obj in objs if rs.ObjectLayer(obj) not in EXCLUDED_LAYERS]
    if not filtered_objs:
        rs.MessageBox("No valid objects found.")
        return

    details = [Detail(obj) for obj in filtered_objs]
    spec = Specification(details)
    dialog = SimpleCutlistDialog()
    dialog.setData(spec)
    Rhino.UI.EtoExtensions.ShowSemiModal(dialog, Rhino.RhinoDoc.ActiveDoc, Rhino.UI.RhinoEtoApp.MainWindow)

if __name__ == "__main__":
    main()

Simple_Cutlist.py (5.9 KB)


CL_Material_Color.3dm (447.6 KB)

1 Like

Hi @Cumberland,

I played around with CellFormatting event but couldn’t get the individual cells working… only rows… I then was able to adopt the code from the latest Eto Samples and create a cell that takes a panel and then apply the colors to that panel itself…

Here’s the code update:

#!/usr/bin/env ironpython2.7
# -*- coding: utf8 -*-
# SimpleCutlist.py

import Rhino
import Rhino.Geometry as g
import rhinoscriptsyntax as rs
import Eto.Drawing as drawing
import Eto.Forms as forms
import scriptcontext

# List of layers to exclude
EXCLUDED_LAYERS = ["Booleans", "Dim", "Default", "Walls"]

# Attributes for the cutlist
class attr:
    def __init__(self, name, isEditable, valType):
        self.name = name
        self.isEditable = isEditable
        self.valType = valType

ATTRS = (
    attr("_N", True, str),        # N (number)
    attr("_Name", True, str),     # Name
    attr("Length", True, float),  # Length
    attr("Width", True, float),   # Width
    attr("Thickness", True, float), # Thickness
    attr("Material", True, str),  # Material
)

def computeDims(obj):
    objRef = Rhino.DocObjects.ObjRef(obj)
    brep = objRef.Geometry()
    edges = list(brep.Edges)
    edges.sort(key=lambda edge: edge.GetLength(), reverse=True)
    edge = edges[0]
    origin = edge.PointAtStart
    xPoint = edge.PointAtEnd
    xVec = Rhino.Geometry.Vector3d(xPoint - origin)
    xVec.Unitize()
    face = brep.Faces[0]
    yVec = Rhino.Geometry.Vector3d.CrossProduct(face.NormalAt(0, 0), xVec)
    yVec.Unitize()
    plane = Rhino.Geometry.Plane(origin, xVec, yVec)
    verts = list(brep.Vertices)
    tMatrix = g.Transform.ChangeBasis(g.Plane.WorldXY, plane)
    map(lambda vert: vert.Transform(tMatrix), verts)
    verts = list(map(lambda vert: vert.Location, verts))
    verts.sort(key=lambda vert: vert.X)
    length = verts[-1].X - verts[0].X
    verts.sort(key=lambda vert: vert.Y)
    width = verts[-1].Y - verts[0].Y
    verts.sort(key=lambda vert: vert.Z)
    thickness = verts[-1].Z - verts[0].Z
    return [length, width, thickness]

class Detail:
    def __init__(self, obj):
        self.obj = obj
        self.id = obj
        for attr_obj in ATTRS:
            key = attr_obj.name
            val = rs.GetUserText(str(obj), key)
            if val is None:
                default = "0" if attr_obj.valType in [float, str] and key != "Material" else ""
                setattr(self, key, default)
                rs.SetUserText(str(obj), key, default)
            else:
                setattr(self, key, val)
        self.setDims()
        self.update_material_from_layer()

    def setDims(self):
        dims = computeDims(self.obj)
        self.Length = str(round(dims[0], 2))
        self.Width = str(round(dims[1], 2))
        self.Thickness = str(round(dims[2], 2))
        rs.SetUserText(str(self.id), "Length", self.Length)
        rs.SetUserText(str(self.id), "Width", self.Width)
        rs.SetUserText(str(self.id), "Thickness", self.Thickness)

    def update_material_from_layer(self):
        objRef = Rhino.DocObjects.ObjRef(self.id)
        index = objRef.Object().Attributes.LayerIndex
        layer = scriptcontext.doc.Layers[index]
        self.Material = layer.Name
        self.layer_color = drawing.Color.FromArgb(layer.Color.ToArgb())
        rs.SetUserText(str(self.id), "Material", self.Material)

    def getParams(self):
        return [getattr(self, attr.name) for attr in ATTRS]

    def refresh(self):
        for attr in ATTRS:
            rs.SetUserText(str(self.id), attr.name, getattr(self, attr.name))

class Specification:
    def __init__(self, details):
        self.details = []
        idSet = set(range(len(details)))
        self.table = []
        self.colors = []  # Store layer colors
        while idSet:
            curId = idSet.pop()
            curDet = details[curId]
            curDetails = [curDet]
            for id in list(idSet):
                if curDet.getParams() == details[id].getParams():
                    curDetails.append(details[id])
                    idSet.remove(id)
            self.details.append(curDetails)
            self.table.append(curDet.getParams())
            self.colors.append(curDet.layer_color)  # Store the layer color

class SimpleCutlistDialog(forms.Dialog[bool]):
    def __init__(self):
        self.Title = "Simple Cutlist"
        self.Padding = drawing.Padding(2)
        self.Resizable = True
        
        # Create export button
        self.button = forms.Button(Text="Export CSV")
        self.button.Width = 80  # Set a smaller, specific width
        self.button.Click += self.buttonClick
        
        # Create main layout
        layout = forms.DynamicLayout()
        self.Content = layout
        self.ClientSize = drawing.Size(600, 400)
        
    def create_table(self, spec):
        self.spec = spec
        
        # Create the table layout
        table = forms.TableLayout()
        table.Spacing = drawing.Size(1, 1)
        
        # Add header row
        header_row = forms.TableRow()
        for i, attr_obj in enumerate(ATTRS):
            header_text = attr_obj.name if attr_obj.name[0] != "_" else attr_obj.name[1:]
            label = forms.Label()
            label.Text = header_text
            label.VerticalAlignment = forms.VerticalAlignment.Center
            
            container = forms.Panel()
            container.Padding = drawing.Padding(5, 2)
            container.BackgroundColor = drawing.Colors.LightGrey
            container.Content = label
            
            header_row.Cells.Add(forms.TableCell(container, True))
        
        table.Rows.Add(header_row)
        
        # Add data rows
        for row_idx, row_data in enumerate(spec.table):
            table_row = forms.TableRow()
            
            for col_idx, cell_data in enumerate(row_data):
                # Create the text cell
                label = forms.Label()
                label.Text = str(cell_data)
                label.VerticalAlignment = forms.VerticalAlignment.Center
                
                # Create container
                container = forms.Panel()
                container.Padding = drawing.Padding(5, 2)
                
                # If this is the Material column, set background color to layer color
                if col_idx == 5:  # Material column
                    container.BackgroundColor = spec.colors[row_idx]
                else:
                    # Alternating row colors
                    if row_idx % 2 == 0:
                        container.BackgroundColor = drawing.Colors.White
                    else:
                        container.BackgroundColor = drawing.Colors.FloralWhite
                
                container.Content = label
                table_row.Cells.Add(forms.TableCell(container, True))
            
            table.Rows.Add(table_row)
        
        # Make all rows and cells expand to fill space
        table.Rows.Add(None)
        return table

    def buttonClick(self, sender, e):
        data = [[col.HeaderText for col in self.gridview.Columns]] if hasattr(self, 'gridview') else [["N", "Name", "Length", "Width", "Thickness", "Material"]]
        data.extend(self.spec.table)
        filename = rs.SaveFileName("Save cutlist as", "CSV File (*.csv)|*.csv||")
        if filename:
            with open(filename, 'w') as f:
                for row in data:
                    f.write("|".join(str(item) for item in row) + "\n")

    def setData(self, spec):
        self.spec = spec
        
        # Create the table layout
        table = self.create_table(spec)
        
        # Create a row for the button with None spacers for centering
        button_row = forms.TableLayout()
        button_row.Spacing = drawing.Size(5, 5)
        button_row_layout = forms.TableRow()
        button_row_layout.Cells.Add(None)  # Left spacer
        button_row_layout.Cells.Add(forms.TableCell(self.button, False))  # Button in center
        button_row_layout.Cells.Add(None)  # Right spacer
        button_row.Rows.Add(button_row_layout)
        button_row.Rows.Add(None)  # Bottom spacer
        
        # Set up main layout
        layout = forms.DynamicLayout()
        layout.Add(table)
        layout.Add(button_row)
        
        self.Content = layout

def main():
    objs = rs.GetObjects("Select polysurface objects", 16, preselect=True)
    if not objs:
        return

    filtered_objs = [obj for obj in objs if rs.ObjectLayer(obj) not in EXCLUDED_LAYERS]
    if not filtered_objs:
        rs.MessageBox("No valid objects found.")
        return

    details = [Detail(obj) for obj in filtered_objs]
    spec = Specification(details)
    dialog = SimpleCutlistDialog()
    dialog.setData(spec)
    Rhino.UI.EtoExtensions.ShowSemiModal(dialog, Rhino.RhinoDoc.ActiveDoc, Rhino.UI.RhinoEtoApp.MainWindow)

if __name__ == "__main__":
    main()

Does that work for your purposes?

I have not tested on R7 or Mac