require 'rexml/document' require 'rexml/formatters/pretty' module XRForge MODEL_EXT = ['.glb', '.gltf', '.blend', '.usdz', '.obj', '.dae', '.x3d'] def self.log(message, filename) # Append the log entry to the log file File.open(filename, 'a') do |file| file.write("#{message}\n") end puts("#{message}\n") end # ============================================================================== # 1. JSON (Ruby Hash) to XML String # ============================================================================== # Internal recursive helper for converting Hash/Array structure to REXML. # Handles attribute conversion (@key -> attribute), hash nesting, and arrays # (which create multiple sibling elements of the same name). # # @param data [Hash, Array, String] The data fragment. # @param p [REXML::Element] The parent REXML element. def self.j2x_rec(data, p) # Check if data is a Hash (for attributes and children) if data.is_a?(Hash) data.each { |k, v| k.start_with?('@') ? p.attributes[k[1..-1]] = v.to_s : j2x_rec(v, p.add_element(k)) } # Check if data is an Array (for repeated elements) elsif data.is_a?(Array) # The parent in this context (e.g., ) is already created. For each array item, # we need to create a new element with the same name as the current parent's *name* # and attach it to the parent's *parent*. # This requires looking up the parent's parent, which breaks terseness. # We must use a simpler recursive structure for compactness. # We assume array structure requires us to create a new element named by the # parent, and assign attributes/children recursively. data.each { |v| j2x_rec(v, p) } # Recursive call to continue processing children # Handle text content else p.text = data.to_s end end # The entry point for converting Ruby Hash to XML string. # # @param data [Hash] The root hash structure. # @return [String] The resulting compact XML string. def self.json2xml(data) # Corrected recursive logic for arrays that are values (e.g., "obj" => [..]) j2x_map = lambda do |d, p_name, p| if d.is_a?(Hash) d.each { |k, v| k.start_with?('@') ? p.attributes[k[1..-1]] = v.to_s : j2x_map.call(v, k, p.add_element(k)) } elsif d.is_a?(Array) # If the value is an array, we iterate, create a sibling element for each, and recurse d.each { |v| j2x_map.call(v, p.name, p.parent.add_element(p.name)) } p.remove # Remove the placeholder element created before hitting the array else p.text = d.to_s end end # Initialize Document and process the single root key doc = REXML::Document.new data.each { |k, v| j2x_map.call(v, k, doc.add_element(k)) } # Output with 2-space indentation o = ""; f = REXML::Formatters::Default.new; f.indentation = 2; f.write(doc.root, o); o.strip end # ============================================================================== # 2. XML String to JSON (Ruby Hash) # ============================================================================== # Internal recursive helper for converting REXML Element to Hash structure. # Handles attribute conversion (attribute -> @key), text, and array creation for siblings. # # @param e [REXML::Element] The XML element. # @return [Hash, String] The resulting hash or simple text value. def self.x2j_rec(e) h = {}; e.attributes.each { |k, v| h["@#{k}"] = v } # Attributes first e.elements.each do |c| # Iterate children v = x2j_rec(c) if h.key?(c.name) h[c.name] = [h[c.name]] unless h[c.name].is_a?(Array) h[c.name] << v else h[c.name] = v end end # Check for text content if no children were processed t = e.get_text.to_s.strip; return t if t && !t.empty? && h.empty?; return h end # The entry point for converting XML string to Ruby Hash. # # @param xmlstr [String] The XML string. # @return [Hash] The resulting Ruby hash structure. def self.xml2json(xmlstr) doc = REXML::Document.new(xmlstr) root = doc.root { root.name => x2j_rec(root) } end def self.getExperienceFile(datapackage, dir, logfile) model_file = nil # Initialize model_file to nil base_name = "" # Extract the desired field (assuming the field is named 'model_file') thumb_file = datapackage['image'] if thumb_file && ! thumb_file.empty? # HEURISTIC: XR Fragments sidecarfile https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File # Get the base name of the thumbnail file without its extension base_name = File.basename(thumb_file, File.extname(thumb_file)) XRForge.log("✅ thumbnail sidecar-file '#{thumb_file}' detected", logfile) # Loop over the list of extensions MODEL_EXT.each do |ext| # Construct the filename with the current extension filename = "#{base_name}#{ext}" # Check if the file exists if File.exist?(filename) XRForge.log("✅ 3D file '#{filename}' detected (heuristic: XRFragment sidecarfile)", logfile) model_file = "#{dir.gsub("/mnt/","")}/#{filename}" # Store the found filename break # Stop the loop once a file is found else # Log a message for the file that was not found, but don't stop XRForge.log("⚠️ 3D file '#{filename}' not detected", logfile) end end else # HEURISTIC: get first suitable 3D file datapackage['resources'].each do |resource| ext = File.extname(resource['path']) if MODEL_EXT.include?( ext ) model_file = resource['path'] XRForge.log("✅ 3D file '#{model_file}' detected (heuristic: first-found)", logfile) break end end end return model_file end end