refactor ruby scripts + updated templates + janus refactor

This commit is contained in:
Leon van Kammen 2025-12-04 23:47:20 +01:00
parent 84a2812ad5
commit 6a13e0c2cc
9 changed files with 318 additions and 181 deletions

View file

@ -20,7 +20,8 @@ begin
end
# Change the directory
dir = File.dirname(filename)
dir = File.dirname(filename)
dirPublic = dir.gsub("/mnt/","").gsub("#","%23")
Dir.chdir( File.dirname(filename) )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
@ -31,37 +32,12 @@ begin
XRForge.log("✅ starting build janusXR XR scene", logfile)
# Extract the desired field (assuming the field is named 'model_file')
thumb_file = data['image']
XRForge.log("✅ thumbnail sidecar-file '#{thumb_file}' detected", logfile)
# Get the base name of the thumbnail file without its extension
base_name = File.basename(thumb_file, File.extname(thumb_file))
model_file = nil # Initialize model_file to nil
# Loop over the list of extensions
XRForge::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", 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
model_file = XRForge.getExperienceFile(data,dir,logfile)
# Check if a model file was found after the loop
if model_file
XRForge.log("✅ Final model file: '#{model_file}'", logfile)
else
XRForge.log("❌ No suitable 3D file found for XR Fragments- / JanusXR-compatible experience", logfile)
if ! model_file
XRForge.log("❌ No suitable 3D file found for JanusXR-compatible experience", logfile)
exit 0
end
# Get the value of the environment variable FEDERATE_DRIVE_HOST
@ -70,7 +46,7 @@ begin
autogenerate = true
if data['description'] && data['description'].match(JMLHeuristic)
if data['description'].match(/autogenerate=['"]false['"]/)
if data['description'].match(/autogenerate=["']false["']/i)
XRForge.log("✅ autogenerate='false' found in JML..keeping this JML",logfile)
autogenerate = false
jmlMatch = data['description'].match(JMLHeuristic)
@ -78,28 +54,62 @@ begin
end
end
if autogenerate
assets = "<assetobject id=\"experience\" src=\"#{federate_drive_host}/#{dirPublic}/#{model_file.gsub("#","%23")}\"/>\n"
objects = ""
# detect audio track sidecarfile as per XR Fragment spec: https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File
xrf_ext = File.extname(model_file)
xrf_base = File.basename(model_file, xrf_ext)
data['resources'].each do |resource|
ext = File.extname(resource['path'])
if ext.match(/mp3/i)
base = File.basename(resource['path'], ext)
if base == xrf_base
XRForge.log("✅ XR Fragments side-car file found: #{resource['path']}",logfile)
assets = assets + " <assetsound id=\"soundtrack\" src=\"#{federate_drive_host}/#{dirPublic}/#{resource["path"]}\"/>\n"
objects = objects + "<sound id=\"soundtrack\" loop=\"true\"/>\n"
end
end
end
private = data['keywords'].include?('singleuser') ? "private='true'" : ""
# tags to JML rooms *REFACTOR PLEASE*
use_local_asset = ""
use_local_asset = data['keywords'].include?('room1') ? "use_local_asset=\"room1\"" : use_local_asset
use_local_asset = data['keywords'].include?('room2') ? "use_local_asset=\"room2\"" : use_local_asset
use_local_asset = data['keywords'].include?('room3') ? "use_local_asset=\"room3\"" : use_local_asset
use_local_asset = data['keywords'].include?('room4') ? "use_local_asset=\"room4\"" : use_local_asset
use_local_asset = data['keywords'].include?('room5') ? "use_local_asset=\"room5\"" : use_local_asset
use_local_asset = data['keywords'].include?('room6') ? "use_local_asset=\"room5\"" : use_local_asset
use_local_asset = data['keywords'].include?('room1_pedestal') ? "use_local_asset=\"room1_pedestal\"" : use_local_asset
use_local_asset = data['keywords'].include?('room2_pedestal') ? "use_local_asset=\"room2_pedestal\"" : use_local_asset
use_local_asset = data['keywords'].include?('room2_narrow') ? "use_local_asset=\"room3_narrow\"" : use_local_asset
objects = objects + " <object pos=\"0 0 5\" rotation=\"-180 0 180\" lighting=\"false\" collision_id=\"experience\" id=\"experience\" />"
jml = <<~JML
<FireBoxRoom>
<Assets>
<assetobject id="experience" src="#{federate_drive_host}/#{model_file.gsub("#","%23")}"/>
#{assets}
</Assets>
<Room autogenerate="true" #{ data['keywords'].include?('singleuser') ? "private='true'" : ""}>
<object pos="0 0 5" rotation="-180 0 180" lighting="false" collision_id="experience" id="experience" />
<Room autogenerate="true" #{use_local_asset} #{private}>
#{objects}
</Room>
</FireBoxRoom>
<!-- archive.org hints -->
<a href="#{federate_drive_host}/#{model_file.gsub("#","%23")}"></a>
<a href="#{federate_drive_host}/#{dirPublic}/#{model_file.gsub("#","%23")}"></a>
JML
data['description'] = data['description'] ? data['description'] : "your description here\n"
data['description'] = <<~DESCRIPTION#{data['description']}
<!-- Hi there! Below is autogenerated JanusXR Markup (JML). -->
<!-- If you want to tweak it, then first disable autogeneration -->
<!-- How? make sure to set autogenerate="false" (see room-tag below) -->
<!-- -->
<!-- JML info: -->
<!-- https://coderofsalvation.github.io/janus-guide/#/examples/markup -->
<!-- https://janusxr.org/docs/build/introtojml/index.html -->
<!-- Hi there! Below is autogenerated JanusXR Markup (JML). -->
<!-- If you want to tweak it, then first disable autogeneration -->
<!-- How? make sure to set autogenerate to "false" (see room-tag below) -->
<!-- -->
<!-- JML info: -->
<!-- https://coderofsalvation.github.io/janus-guide/#/examples/markup -->
<!-- https://janusxr.org/docs/build/introtojml/index.html -->
#{jml}
DESCRIPTION

View file

@ -30,37 +30,12 @@ begin
XRForge.log("✅ starting build mml XR scene", logfile)
# Extract the desired field (assuming the field is named 'model_file')
thumb_file = data['image']
XRForge.log("✅ thumbnail sidecar-file '#{thumb_file}' detected", logfile)
# Get the base name of the thumbnail file without its extension
base_name = File.basename(thumb_file, File.extname(thumb_file))
model_file = nil # Initialize model_file to nil
# Loop over the list of extensions
XRForge::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", 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
model_file = XRForge.getExperienceFile(data,dir,logfile)
# Check if a model file was found after the loop
if model_file
XRForge.log("✅ Final model file: '#{model_file}'", logfile)
else
XRForge.log("❌ No suitable 3D file found for XR Fragments- / MML-compatible experience", logfile)
if ! model_file
XRForge.log("❌ No suitable 3D file found for MML-compatible experience", logfile)
exit 0
end
# Get the value of the environment variable FEDERATE_DRIVE_HOST

View file

@ -13,36 +13,20 @@ filename = ARGV[0]
begin
# Change the directory
Dir.chdir( File.dirname(filename) )
dir = File.dirname(filename)
Dir.chdir( dir )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
XRForge.log("✅ starting XR fragments check", logfile)
# Extract the desired field (assuming the field is named 'model_file')
thumb_file = data['image']
XRForge.log("✅ thumbnail sidecar-file '#{thumb_file}' detected", logfile)
model_file = XRForge.getExperienceFile(data,dir,logfile)
# Get the base name of the thumbnail file without its extension
base_name = File.basename(thumb_file, File.extname(thumb_file))
model_file = nil # Initialize model_file to nil
# Loop over the list of extensions
XRForge::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", logfile)
model_file = 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
# Check if a model file was found after the loop
if ! model_file
XRForge.log("❌ No suitable 3D file found for XR Fragments-compatible experience", logfile)
exit 0
end
# Check if a model file was found after the loop

View file

@ -106,4 +106,43 @@ module XRForge
{ 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

View file

@ -0,0 +1,52 @@
<%= form_with model: @model do |form| %>
<%= text_input_row form, :name %>
<%= render "tags_edit", form: form, name: "model[tag_list]", value: (@model.tags.order(taggings_count: :desc, name: :asc).map { |tag| tag.name }).join(","), label: translate(".tags"), tags: @available_tags %>
<div style="margin-left: 97px; max-height: 333px; overflow: scroll; margin-bottom: 50px; border: 1px solid #CCC; border-radius: 5px; margin-right:10px; background:black; padding:20px;">
<b>Available tags:</b><br>
<%= render 'models/tags_info' %>
</div>
<%= collection_select_input_row form, :preview_file, @model.valid_preview_files, :id, :name, help: t(".preview_file.help") %>
<%= collection_select_input_row form,
:creator, @creators, :id, :name_with_domain,
include_blank: true,
selected: @default_creator&.id,
button: (if policy(:creator).new?
{
path: new_creator_path,
label: t("creators.general.new")
}
end) %>
<% if SiteSettings.show_libraries || current_user.is_administrator? %>
<%= unless @model.contains_other_models?
collection_select_input_row form,
:library, policy_scope(Library).all, :id, :name,
include_blank: true
end %>
<% end %>
<%= collection_select_input_row form,
:collection, @collections, :id, :name_with_domain,
include_blank: true,
button: (if policy(:collection).new?
{
path: new_collection_path,
label: t("collections.general.new")
}
end) %>
<%= render "links_form", form: form %>
<%= text_input_row form, :caption %>
<%= rich_text_input_row form, :notes, help: t(".notes.help_html") %>
<%= select_input_row form, :license, license_select_options(selected: @model.license), include_blank: true %>
<%= checkbox_input_row form, :sensitive %>
<%= select_input_row form, :indexable, indexable_select_options(form.object) %>
<%= select_input_row form, :ai_indexable, ai_indexable_select_options(form.object) if SiteSettings.allow_ai_bots %>
<%= render "caber_relations_form", form: form %>
<%= form.submit "Save", class: "btn btn-primary" %>
<% end %>

View file

@ -1,5 +1,3 @@
Features can be toggled via tags:<br>
<br>
<table class="table">
<tr>
<td>
@ -12,21 +10,32 @@
</tr>
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">janusxr</a>
<a class="badge rounded-pill bg-secondary tag">your@mastodon.online</a>
</td>
<td>
This generates an JanusXR address.<br>
<a href="https://janusxr.org/" target="_blank">JanusXR</a> is an Open Immersive Web-layer since 2015, which allows existing webpages to become spatial.<br>
It is Free and Opensource, and allows you to meet others in this experience (avatars, chat and voice etc).<br>
Simply enter your mastodon address as tag, and XRForge will generate a 3D model, based on your last post on mastodon.<br>
</td>
</tr>
</table>
<Hr/>
<b>JanusXR tags:</b>
<table class="table">
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">mml</a>
<td style="vertical-align:top">
<a class="badge rounded-pill bg-secondary tag">room1</a><br>
<a class="badge rounded-pill bg-secondary tag">room2</a><br>
<a class="badge rounded-pill bg-secondary tag">room3</a><br>
<a class="badge rounded-pill bg-secondary tag">room4</a><br>
<a class="badge rounded-pill bg-secondary tag">room5</a><br>
<a class="badge rounded-pill bg-secondary tag">room6</a><br>
<a class="badge rounded-pill bg-secondary tag">room1_pedestal</a><br>
<a class="badge rounded-pill bg-secondary tag">room2_pedestal</a><br>
<a class="badge rounded-pill bg-secondary tag">room2_narrow</a><br>
</td>
<td>
This generates an MML address.<br>
Metaverse Markup Language (<a href="https://mml.io" target="_blank">MML</a>) is an open markup language used to define experiences.
This wraps a room around the 3D asset:<br><Br>
<img src="/assets/jml_templates.png" style="width:100%; max-width:800px"/>
</td>
</tr>
<tr>

View file

@ -46,19 +46,81 @@
<% end %>
<% end %>
</div>
<div class="col-md-3" id="sidebar">
<%= card :secondary, t(".model_details") do %>
<%= card :secondary, "XR networks" do %>
<table class="table table-borderless table-sm">
<% if ENV['FEDERATE_DRIVE_HOST'].present? %>
<% if @model.tags.where(name: "xrfragments" ).any? %>
<tr>
<td>
<img src="/assets/janusxr.svg" style="width: 16px; transform: translate(0px,-2px);">
</td>
<td>
<%
fediURL = ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/janusxr.html"
%>
<%= link_to "JanusXR", fediURL, target:"_blank" %>
<a href="" target="_blank"><i class="bi bi-link-45deg"></i></a>
<label for="toggle_janusxr"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_janusxr" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
Click to view this page in a JanusXR (JML) browser like JanusWeb.<br>
<a href="https://janusxr.org/" target="_blank">JanusXR</a> is an Open XR content-weblayer since 2013, which allows existing webpages to become spatial.<br>
It is Free and Opensource, and allows you to meet others in this experience (avatars, chat and voice etc).<br>
</small>
</div>
</div>
</td>
</tr>
<%
# lets generate a JanusXR spatial layer for the current scene
jmlfile = @model.library.path+"/"+@model.path+"/.xrforge/scene.jml"
jml = ""
if File.exist?(jmlfile)
jml = File.read(jmlfile)
end
%>
<!-- JML (https://janusxr.org) spatial markup -->
<%== jml %>
<% end %>
<% if @model.tags.where(name: "xrfragments" ).any? %>
<tr>
<td>⁂</td>
<td>
<%= link_to "XR Fragments", ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.gltf" %>
<label for="toggle_gltf"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_gltf" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the glTF JSON URL with embedded hyperlinks for immersive navigation to other 3D scene-files, as per the <a href="https://xrfragment.org" target="_blank">XR Fragments</a> spec.<br>
</small>
</div>
</div>
</td>
</tr>
<% end %>
<% if SiteSettings.federation_enabled? %>
<tr>
<td>⁂</td>
<td><% if @model.remote? %>
<small><%= link_to @model.federails_actor.at_address, @model.federails_actor.profile_url, target: "new" %></small>
<%= link_to @model.federails_actor.at_address, @model.federails_actor.profile_url, target: "new" %>
<% else %>
<small>
<%= @model.federails_actor.short_at_address %>
<%= render Components::CopyButton.new(text: @model.federails_actor.at_address) %>
</small>
<% end %>
<label for="toggle_activitypub"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
@ -74,62 +136,12 @@
</td>
</tr>
<% end %>
<% if @model.creator %>
<tr>
<td><%= icon "person", Creator.model_name.human %></td>
<td><%= link_to @model.creator.name, @model.creator, itemprop: "author" %></td>
</tr>
<% end %>
<% if ENV['FEDERATE_DRIVE_HOST'].present? %>
<% if @model.tags.where(name: "xrfragments" ).any? %>
<tr>
<td>
<img src="/assets/janusxr.svg" style="width: 16px; transform: translate(0px,-2px);">
</td>
<td>
<%
fediURL = ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/janusxr.html"
%>
<%= link_to "JanusXR address", fediURL, target:"_blank" %>
<a href="" target="_blank"><i class="bi bi-link-45deg"></i></a>
<label for="toggle_janusxr"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_janusxr" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the JanusXR address.<br>
<a href="https://janusxr.org/" target="_blank">JanusXR</a> is an Open Immersive Web-layer since 2015, which allows existing webpages to become spatial.<br>
It is Free and Opensource, and allows you to meet others in this experience (avatars, chat and voice etc).<br>
</small>
</div>
</div>
</td>
</tr>
<%
# lets generate a JanusXR spatial layer for the current scene
jmlfile = @model.library.path+"/"+@model.path+"/.xrforge/scene.jml"
jml = ""
if File.exist?(jmlfile)
jml = File.read(jmlfile)
end
%>
<!-- JML (https://janusxr.org) spatial markup -->
<%== jml %>
<% end %>
<% if @model.tags.where(name: "xrfragments" ).any? %>
<tr>
<td>
<i class="bi bi-eye-fill" role="img"></i>
</td>
<td>⁂</td>
<td>
<%= link_to "MML address", "https://viewer.mml.io/main/v1/?url="+URI.encode_www_form_component(ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.mml"), target:"_blank" %>
<%= link_to "MML", "https://viewer.mml.io/main/v1/?url="+URI.encode_www_form_component(ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.mml"), target:"_blank" %>
<a href="<%= ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.mml" %>" target="_blank"><i class="bi bi-link-45deg"></i></a>
<label for="toggle_mml"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
@ -145,7 +157,20 @@
</td>
</tr>
<% end %>
<% end %>
</table>
<% end %>
<%= card :secondary, t(".model_details") do %>
<table class="table table-borderless table-sm">
<% if @model.creator %>
<tr>
<td><%= icon "person", Creator.model_name.human %></td>
<td><%= link_to @model.creator.name, @model.creator, itemprop: "author" %></td>
</tr>
<% end %>
<tr>
<td>
<i class="bi bi-journal-check" role="img"></i>
@ -160,6 +185,8 @@
<small>
This is the build log of XR Forge.<br>
When you add files, they are processed, validated (for <a href="https://xrfragment.org" target="_blank">XR Fragment</a> compliance).<br>
Features can be toggled via tags:<br>
<br>
<%= render 'models/tags_info' %>
</small>
</div>
@ -187,27 +214,6 @@
</div>
</td>
</tr>
<!--
<tr>
<td>
<i class="bi bi-filetype-json" role="img"></i>
</td>
<td>
<%= link_to "glTF JSON", ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.gltf" %>
<label for="toggle_gltf"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_gltf" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the glTF JSON URL.<br>
</small>
</div>
</div>
</td>
</tr>
-->
<% end %>
<% if @model.collection %>
<tr>
@ -282,6 +288,7 @@
<div class="mb-3 w-100 text-center">
<%= render Components::DownloadButton.new(model: @model) %>
</div>
<%= card :secondary, t("layouts.card_list_page.actions_heading") do %>
<%= render Components::ReportButton.new(object: @model, path: new_model_report_path(@model)) %>

View file

@ -45,3 +45,64 @@ Rails.application.config.to_prepare do
end
end
#class XRForgeRoutes
# # Rack endpoints must implement a #call method that accepts the Rack environment hash (env).
# def call(env)
# # Wrap the environment in a Rails Request object for easier parameter access
# request = ActionDispatch::Request.new(env)
#
# # 1. Access the model ID from the route parameters
# # The route path parameters are stored in the routing information
# model_id = request.path_parameters[:id]
#
# begin
# # 2. Find the model
# # Note: We are outside the controller, so we access Model directly.
# model = Model.find(model_id)
#
# puts "-------------------------"
# Rails.logger.info("build process for Model ID: #{model.id}")
# url_helpers = Rails.application.routes.url_helpers
# redirect_path = url_helpers.model_path(model)
#
# # 302 Found response for a temporary redirect
# [
# 302,
# {
# 'Content-Type' => 'text/html',
# 'Location' => redirect_path
# },
# ['Build process started successfully.']
# ]
#
# rescue ActiveRecord::RecordNotFound
# # Handle model not found error
# [
# 404,
# { 'Content-Type' => 'text/plain' },
# ["Model with ID #{model_id} not found."]
# ]
# rescue => e
# # Handle other errors
# Rails.logger.error("Build Rack App Error: #{e.message}")
# [
# 500,
# { 'Content-Type' => 'text/plain' },
# ["Internal Server Error: Could not initiate build."]
# ]
# end
# end
#end
#
#Rails.configuration.to_prepare do
# Rails.application.routes.draw do
# resources :models, only: [] do
# member do
# # Point the route directly to an instance of the Rack application class.
# # The route is: POST /models/:id/build
# match 'build', to: XRForgeRoutes.new, via: :post
# end
# end
# end
#end

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB