sketchucation logo sketchucation
    • Login
    Oops, your profile's looking a bit empty! To help us tailor your experience, please fill in key details like your SketchUp version, skill level, operating system, and more. Update and save your info on your profile page today!
    ⚠️ Important | Libfredo 15.6a introduces important bugfixes for Fredo's Extensions Update

    SketchVault

    Scheduled Pinned Locked Moved Developers' Forum
    2 Posts 2 Posters 34 Views 2 Watching
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • majidM Offline
      majid
      last edited by

      I have been developing a vault generator add-on for Blender 3D that is aimed to mimic RhinoVault (considering Blender cons/pros vs Rhino).
      It tries to find the best compression-only form for the given input mesh. Recently, I used Gemini to translate it to work with SketchUp. It is in its early stages and would be fine if anyone helps me to improve it.

      # ============================================================
      # SKETCHUP FOLDING VAULTS V4 (Robust Non-Flat Mesh + Red Anchors) | By Majid Yeganegi AI optimization -2025 Nov.
      # ============================================================
      
      require 'sketchup.rb'
      
      module VaultPlugin
      
        # --- 1. Math Helper Classes (Unchanged for performance) ---
        class SparseMatrix
          attr_reader :n_rows
          def initialize(n_rows)
            @n_rows = n_rows
            @rows = Hash.new { |h, k| h[k] = {} }
          end
          def add(r, c, val)
            return if r >= @n_rows || c >= @n_rows
            @rows[r][c] ||= 0.0
            @rows[r][c] += val
          end
          def dot(vec)
            result = Array.new(@n_rows, 0.0)
            @rows.each do |r_idx, col_data|
              sum = 0.0
              col_data.each { |c_idx, val| sum += val * vec[c_idx] }
              result[r_idx] = sum
            end
            result
          end
        end
      
        class VectorMath
          def self.sub(a, b); a.zip(b).map { |x, y| x - y }; end
          def self.add(a, b); a.zip(b).map { |x, y| x + y }; end
          def self.scale(vec, s); vec.map { |x| x * s }; end
          def self.dot(a, b); a.each_with_index.reduce(0) { |sum, (val, i)| sum + val * b[i] }; end
        end
      
        # --- 2. CG Solver ---
        def self.cg_solve(matrix, b, x0, max_iter, tol)
          x = x0.dup
          r = VectorMath.sub(b, matrix.dot(x))
          p = r.dup
          rsold = VectorMath.dot(r, r)
      
          max_iter.times do
            ap = matrix.dot(p)
            p_ap = VectorMath.dot(p, ap)
            break if p_ap.abs < 1e-15 
      
            alpha = rsold / p_ap
            x = VectorMath.add(x, VectorMath.scale(p, alpha))
            r = VectorMath.sub(r, VectorMath.scale(ap, alpha))
            rsnew = VectorMath.dot(r, r)
            break if Math.sqrt(rsnew) < tol
      
            p = VectorMath.add(r, VectorMath.scale(p, rsnew / rsold))
            rsold = rsnew
          end
          return x
        end
      
        # --- 3. Helper: Check if Material is RED ---
        def self.is_red?(entity)
          return false unless entity.material
          c = entity.material.color
          # Check for Pure Red (255, 0, 0) or close to it
          return true if c.red > 200 && c.green < 50 && c.blue < 50
          return false
        end
      
        # --- 4. Main Process ---
        def self.process_mesh(selection_mesh, density_input, load_input, scale_factor, iterations)
          model = Sketchup.active_model
          entities = model.active_entities
      
          # A. Topology Extraction
          verts = []
          v_to_id = {}
          faces = selection_mesh.grep(Sketchup::Face)
          
          if faces.empty?
            UI.messagebox("Please select a Mesh (Faces) first.")
            return
          end
      
          faces.each do |f|
            f.vertices.each do |v|
              unless v_to_id.key?(v.entityID)
                v_to_id[v.entityID] = verts.length
                verts << v
              end
            end
          end
          n = verts.length
      
          # B. Explicit Anchor Detection (THE "RED" RULE ONLY)
          anchors = []
          
          # Check 1: Red Faces
          faces.each { |f| anchors.concat(f.vertices.map { |v| v_to_id[v.entityID] }) if is_red?(f) }
      
          # Check 2: Red Edges
          edges = faces.map { |f| f.edges }.flatten.uniq
          edges.each do |e|
            if is_red?(e)
              anchors << v_to_id[e.start.entityID]
              anchors << v_to_id[e.end.entityID]
            end
          end
      
          anchors = anchors.uniq
      
          if anchors.empty?
            UI.messagebox("Error: No RED anchors found! The mesh will float freely.\nPlease paint at least one edge or face RED.")
            return
          end
      
          # C. Index Mapping
          free_map = []
          global_to_free = {}
          free_count = 0
          
          (0...n).each do |i|
            unless anchors.include?(i)
              free_map << i
              global_to_free[i] = free_count
              free_count += 1
            end
          end
          n_free = free_map.length
          
          if n_free == 0
            UI.messagebox("Error: All selected vertices are anchors. Nothing to calculate.")
            return
          end
      
          # D. Build Edges Index
          edges_idx = edges.map do |e|
            id1 = v_to_id[e.start.entityID]
            id2 = v_to_id[e.end.entityID]
            [id1, id2] if id1 && id2
          end.compact
      
          # E. TNA Setup & Matrix Assembly
          q_val = density_input 
          l_ff = SparseMatrix.new(n_free)
          d = Array.new(n, 0.0)
      
          # Build Laplacian (L_ff) and Diagonal (d)
          edges_idx.each do |u, v|
            w = q_val
            d[u] += w
            d[v] += w
            
            u_free = !anchors.include?(u)
            v_free = !anchors.include?(v)
      
            if u_free && v_free
              uf, vf = global_to_free[u], global_to_free[v]
              l_ff.add(uf, vf, -w)
              l_ff.add(vf, uf, -w)
            end
          end
      
          free_map.each { |i_glob| l_ff.add(global_to_free[i_glob], global_to_free[i_glob], d[i_glob]) }
      
          # F. Vertical Equilibrium (Solve for Final Z: Z_f)
          
          # Initial Z-positions of free nodes (used as the initial guess and for calculating Delta Z later)
          z_orig_free = Array.new(n_free)
          free_map.each_with_index { |glob_idx, i| z_orig_free[i] = verts[glob_idx].position.z }
      
          z_current = z_orig_free.dup
          
          # Iterative Solve for Z_f
          iterations.times do
            rhs = Array.new(n_free, -load_input) # -Pf
            
            edges_idx.each do |u, v|
              w = q_val
              u_free = !anchors.include?(u)
              v_free = !anchors.include?(v)
      
              # Contribution of fixed anchors to the RHS (-Lfa * Za)
              if u_free && !v_free
                rhs[global_to_free[u]] += w * verts[v].position.z 
              elsif !u_free && v_free
                rhs[global_to_free[v]] += w * verts[u].position.z
              end
            end
            
            z_current = cg_solve(l_ff, rhs, z_current, 200, 1e-5)
          end
          
          # G. Calculate and Apply Displacement (Delta Z)
          
          # Delta Z = Z_final - Z_original. This is the displacement vector.
          # It will be negative for a hanging form (since Z_f < Z_orig_f).
          delta_z = VectorMath.sub(z_current, z_orig_free)
          
          model.start_operation("Generate Vault", true)
          
          group = entities.add_group
          mesh_out = Geom::PolygonMesh.new
          
          final_pts = []
          
          # Use the sign of the scale factor to determine the flip
          # scale_factor = -5.0 means: flip the negative displacement (to positive) and scale by 5.0
          scale = scale_factor 
          
          (0...n).each do |i|
            v_orig = verts[i].position
            if anchors.include?(i)
              final_pts << v_orig # Anchors stay at their original Z
            else
              dz_calc = delta_z[global_to_free[i]]
              
              # Z_final = Z_original + (Delta Z * Scale)
              # If Delta Z is negative (hanging form) and Scale is negative (-5.0),
              # the result is positive displacement, creating the arch/vault.
              z_final = v_orig.z + (dz_calc * scale)
              
              final_pts << [v_orig.x, v_orig.y, z_final]
            end
          end
      
          # Reconstruct faces
          faces.each do |f|
            idxs = f.vertices.map { |v| v_to_id[v.entityID] }
            mesh_out.add_polygon(idxs.map { |k| final_pts[k] })
          end
          
          group.entities.add_faces_from_mesh(mesh_out)
          group.name = "Vault_Result"
          group.material = "White"
          
          group.entities.grep(Sketchup::Edge).each { |e| e.soft = true; e.smooth = true }
      
          model.commit_operation
        end
      
        # --- 5. UI Dialog ---
        def self.show_dialog
          prompts = ["Force Density (Stiffness)", "Load", "Height Scale (Multiplier)", "Iterations"]
          # Default Scale set to 5.0, as requested.
          defaults = [1.0, 1.0, 5.0, 30] 
          
          input = UI.inputbox(prompts, defaults, "TNA Settings")
          return unless input
          
          sel = Sketchup.active_model.selection
          self.process_mesh(sel, input[0], input[1], input[2], input[3])
        end
      
        unless file_loaded?(__FILE__)
          menu = UI.menu('Plugins')
          menu.add_item('Create Vault (TNA)') { self.show_dialog }
          file_loaded(__FILE__)
        end
      end
      

      SketchUp_7T40AZMCI0.png

      My inspiring A, B, Sketches book: https://sketchucation.com/shop/books/intermediate/2612-alphabet-inspired-sketches--inspiring-drills-for-architects--3d-artists-and-designers-

      1 Reply Last reply Reply Quote 0
      • TIGT Offline
        TIG Moderator
        last edited by

        Majid

        I'll look at it for you and send a fixed version by PM - it might be early next week...

        TIG

        1 Reply Last reply Reply Quote 0
        • 1 / 1
        • First post
          Last post
        Buy SketchPlus
        Buy SUbD
        Buy WrapR
        Buy eBook
        Buy Modelur
        Buy Vertex Tools
        Buy SketchCuisine
        Buy FormFonts

        Advertisement