exrviewer 0.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +55 -0
- data/app/assets/config/exr_viewer_manifest.js +1 -0
- data/app/assets/javascripts/exr-viewer-stimulus.js +706 -0
- data/lib/exr_viewer/active_storage/exr_previewer.rb +101 -0
- data/lib/exr_viewer/helpers.rb +176 -0
- data/lib/exr_viewer/railtie.rb +33 -0
- data/lib/exr_viewer/version.rb +5 -0
- data/lib/exr_viewer.rb +9 -0
- data/lib/generators/exr_viewer/install/install_generator.rb +243 -0
- data/lib/generators/exr_viewer/install/templates/controllers_application.js +7 -0
- data/lib/generators/exr_viewer/install/templates/controllers_index_for_importmap.js +4 -0
- data/lib/generators/exr_viewer/install/templates/controllers_index_for_node.js +1 -0
- data/lib/generators/exr_viewer/install/templates/exr_viewer_controller.js +707 -0
- metadata +127 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require "active_storage/previewer"
|
|
6
|
+
require "active_storage/errors"
|
|
7
|
+
require "image_processing/vips"
|
|
8
|
+
require "vips"
|
|
9
|
+
|
|
10
|
+
module ExrViewer
|
|
11
|
+
module ActiveStorage
|
|
12
|
+
class ExrPreviewer < ::ActiveStorage::Previewer
|
|
13
|
+
EXR_CONTENT_TYPES = Set.new([
|
|
14
|
+
"image/x-exr",
|
|
15
|
+
"image/exr",
|
|
16
|
+
"application/x-exr",
|
|
17
|
+
"application/exr"
|
|
18
|
+
]).freeze
|
|
19
|
+
|
|
20
|
+
FALLBACK_CONTENT_TYPES = Set.new([
|
|
21
|
+
"application/octet-stream",
|
|
22
|
+
"binary/octet-stream"
|
|
23
|
+
]).freeze
|
|
24
|
+
|
|
25
|
+
MAX_EDGE = 1600
|
|
26
|
+
JPEG_QUALITY = 85
|
|
27
|
+
GAMMA_EXPONENT = 1.0 / 2.2
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def accept?(blob)
|
|
31
|
+
content_type = blob.content_type.to_s.downcase
|
|
32
|
+
return true if EXR_CONTENT_TYPES.include?(content_type)
|
|
33
|
+
|
|
34
|
+
extension = blob.filename.extension_without_delimiter.to_s.downcase
|
|
35
|
+
FALLBACK_CONTENT_TYPES.include?(content_type) && extension == "exr"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def preview(**options)
|
|
40
|
+
download_blob_to_tempfile do |input|
|
|
41
|
+
output = Tempfile.new([blob.filename.base.to_s, ".jpg"], binmode: true)
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
render_preview(input.path, output.path)
|
|
45
|
+
|
|
46
|
+
File.open(output.path, "rb") do |io|
|
|
47
|
+
yield io: io,
|
|
48
|
+
filename: "#{blob.filename.base}.jpg",
|
|
49
|
+
content_type: "image/jpeg",
|
|
50
|
+
**options
|
|
51
|
+
end
|
|
52
|
+
ensure
|
|
53
|
+
output.close!
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
rescue LoadError => e
|
|
57
|
+
raise ::ActiveStorage::UnpreviewableError,
|
|
58
|
+
"EXR previewing requires image_processing and ruby-vips (#{e.message})"
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
raise e if e.is_a?(::ActiveStorage::UnpreviewableError)
|
|
61
|
+
|
|
62
|
+
raise ::ActiveStorage::UnpreviewableError,
|
|
63
|
+
"Unable to generate EXR preview: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def render_preview(input_path, output_path)
|
|
69
|
+
input = ::Vips::Image.new_from_file(input_path, access: :sequential)
|
|
70
|
+
rgb = best_effort_rgb(input)
|
|
71
|
+
rgb = rgb.cast("float")
|
|
72
|
+
rgb = rgb.clamp(min: 0.0, max: 1.0)
|
|
73
|
+
rgb = rgb.pow(GAMMA_EXPONENT)
|
|
74
|
+
|
|
75
|
+
linear_png = Tempfile.new(["exr-linear", ".png"], binmode: true)
|
|
76
|
+
begin
|
|
77
|
+
rgb.pngsave(linear_png.path, compression: 3)
|
|
78
|
+
|
|
79
|
+
ImageProcessing::Vips
|
|
80
|
+
.source(linear_png.path)
|
|
81
|
+
.resize_to_limit(MAX_EDGE, MAX_EDGE)
|
|
82
|
+
.convert("jpg")
|
|
83
|
+
.saver(Q: JPEG_QUALITY, strip: true)
|
|
84
|
+
.call(destination: output_path)
|
|
85
|
+
ensure
|
|
86
|
+
linear_png.close!
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def best_effort_rgb(image)
|
|
91
|
+
if image.bands >= 3
|
|
92
|
+
image.extract_band(0, n: 3)
|
|
93
|
+
elsif image.bands == 2
|
|
94
|
+
image.bandjoin(image.extract_band(0, n: 1))
|
|
95
|
+
else
|
|
96
|
+
image.bandjoin([image, image])
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module ExrViewer
|
|
6
|
+
module Helpers
|
|
7
|
+
COMPONENT_ATTRIBUTES = Set.new([
|
|
8
|
+
"tone-mapping",
|
|
9
|
+
"exposure",
|
|
10
|
+
"controls"
|
|
11
|
+
]).freeze
|
|
12
|
+
|
|
13
|
+
HTML_ATTRIBUTES = Set.new([
|
|
14
|
+
"id",
|
|
15
|
+
"class",
|
|
16
|
+
"style",
|
|
17
|
+
"title",
|
|
18
|
+
"role",
|
|
19
|
+
"hidden",
|
|
20
|
+
"tabindex",
|
|
21
|
+
"lang",
|
|
22
|
+
"dir",
|
|
23
|
+
"slot",
|
|
24
|
+
"part"
|
|
25
|
+
]).freeze
|
|
26
|
+
|
|
27
|
+
ATTRIBUTE_ALIASES = {
|
|
28
|
+
"tone_mapping" => "tone-mapping"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def exr_viewer_include_tag(optional: true, **attrs)
|
|
32
|
+
return "".html_safe if optional && exr_viewer_managed_by_js_pipeline?
|
|
33
|
+
|
|
34
|
+
defaults = { defer: true }
|
|
35
|
+
javascript_include_tag("exr-viewer-stimulus", **defaults.merge(attrs))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def exr_viewer_tag(src:, **attrs)
|
|
39
|
+
normalized = normalize_component_attrs(attrs)
|
|
40
|
+
component = extract_component_attributes(normalized)
|
|
41
|
+
data_attributes = extract_data_attributes(normalized)
|
|
42
|
+
merge_controller_identifier(data_attributes, "exr-viewer")
|
|
43
|
+
|
|
44
|
+
data_attributes["exr-viewer-src-value"] = resolve_exr_src(src)
|
|
45
|
+
data_attributes["exr-viewer-tone-mapping-value"] = component.fetch("tone-mapping", "aces")
|
|
46
|
+
data_attributes["exr-viewer-exposure-value"] = component.fetch("exposure", 1.0)
|
|
47
|
+
data_attributes["exr-viewer-controls-value"] = component.fetch("controls", false) ? true : false
|
|
48
|
+
|
|
49
|
+
normalized["data"] = data_attributes
|
|
50
|
+
merge_class_name(normalized, "exr-viewer")
|
|
51
|
+
|
|
52
|
+
content_tag("div", "", normalized)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def exr_preview_tag(source, preview: {}, **attrs)
|
|
56
|
+
image_tag(resolve_exr_preview_source(source, preview_options: preview), **attrs)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def resolve_exr_src(source)
|
|
62
|
+
return source if source.is_a?(String)
|
|
63
|
+
|
|
64
|
+
if active_storage_blob?(source) || active_storage_attachment?(source)
|
|
65
|
+
return url_for(source)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
raise ArgumentError, "src must be a String, ActiveStorage::Blob, or ActiveStorage::Attachment"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def active_storage_blob?(source)
|
|
72
|
+
defined?(::ActiveStorage::Blob) && source.is_a?(::ActiveStorage::Blob)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def active_storage_attachment?(source)
|
|
76
|
+
defined?(::ActiveStorage::Attachment) && source.is_a?(::ActiveStorage::Attachment)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_exr_preview_source(source, preview_options:)
|
|
80
|
+
return source if source.is_a?(String)
|
|
81
|
+
|
|
82
|
+
if active_storage_blob?(source) || active_storage_attachment?(source)
|
|
83
|
+
return source.preview(**preview_options).processed
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
raise ArgumentError, "source must be a String, ActiveStorage::Blob, or ActiveStorage::Attachment"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def exr_viewer_managed_by_js_pipeline?
|
|
90
|
+
exr_viewer_importmap_setup? || exr_viewer_node_setup?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def exr_viewer_importmap_setup?
|
|
94
|
+
exr_viewer_app_file_exists?("config/importmap.rb")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def exr_viewer_node_setup?
|
|
98
|
+
exr_viewer_app_file_exists?("package.json")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def exr_viewer_app_file_exists?(relative_path)
|
|
102
|
+
return false unless defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
103
|
+
|
|
104
|
+
File.exist?(Rails.root.join(relative_path))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def extract_component_attributes(normalized)
|
|
108
|
+
component = {}
|
|
109
|
+
COMPONENT_ATTRIBUTES.each do |key|
|
|
110
|
+
next unless normalized.key?(key)
|
|
111
|
+
|
|
112
|
+
component[key] = normalized.delete(key)
|
|
113
|
+
end
|
|
114
|
+
component
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def extract_data_attributes(normalized)
|
|
118
|
+
data_attributes = {}
|
|
119
|
+
|
|
120
|
+
data = normalized.delete("data")
|
|
121
|
+
if data.is_a?(Hash)
|
|
122
|
+
data.each do |key, value|
|
|
123
|
+
data_attributes[key.to_s.tr("_", "-")] = value
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
normalized.keys.grep(/\Adata-/).each do |key|
|
|
128
|
+
data_attributes[key.delete_prefix("data-")] = normalized.delete(key)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
data_attributes
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def merge_controller_identifier(data_attributes, identifier)
|
|
135
|
+
current = data_attributes.fetch("controller", "").to_s.split(/\s+/).reject(&:empty?)
|
|
136
|
+
current << identifier unless current.include?(identifier)
|
|
137
|
+
data_attributes["controller"] = current.join(" ")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def merge_class_name(normalized, css_class)
|
|
141
|
+
classes = normalized.fetch("class", "").to_s.split(/\s+/).reject(&:empty?)
|
|
142
|
+
classes << css_class unless classes.include?(css_class)
|
|
143
|
+
normalized["class"] = classes.join(" ")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def normalize_component_attrs(attrs)
|
|
147
|
+
normalized = {}
|
|
148
|
+
|
|
149
|
+
attrs.each do |raw_key, value|
|
|
150
|
+
key = canonical_attr_name(raw_key)
|
|
151
|
+
|
|
152
|
+
unless allowed_attr?(key)
|
|
153
|
+
raise ArgumentError, "Unknown exr_viewer_tag attribute: #{raw_key}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
normalized[key] = value
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
normalized
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def canonical_attr_name(raw_key)
|
|
163
|
+
string_key = raw_key.to_s
|
|
164
|
+
ATTRIBUTE_ALIASES.fetch(string_key, string_key.tr("_", "-"))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def allowed_attr?(key)
|
|
168
|
+
COMPONENT_ATTRIBUTES.include?(key) ||
|
|
169
|
+
HTML_ATTRIBUTES.include?(key) ||
|
|
170
|
+
key.start_with?("data-") ||
|
|
171
|
+
key.start_with?("aria-") ||
|
|
172
|
+
key == "data" ||
|
|
173
|
+
key == "aria"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/core_ext/module"
|
|
5
|
+
|
|
6
|
+
module ExrViewer
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
initializer "exr_viewer.helpers" do
|
|
9
|
+
ActiveSupport.on_load(:action_view) do
|
|
10
|
+
include ExrViewer::Helpers
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer "exr_viewer.assets.precompile" do |app|
|
|
15
|
+
next unless app.config.respond_to?(:assets)
|
|
16
|
+
next unless app.config.assets.respond_to?(:precompile)
|
|
17
|
+
|
|
18
|
+
app.config.assets.precompile << "exr-viewer-stimulus.js"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer "exr_viewer.active_storage.previewer" do
|
|
22
|
+
config.after_initialize do
|
|
23
|
+
next unless defined?(::ActiveStorage)
|
|
24
|
+
next unless ::ActiveStorage.respond_to?(:previewers)
|
|
25
|
+
|
|
26
|
+
previewer = ExrViewer::ActiveStorage::ExrPreviewer
|
|
27
|
+
unless ::ActiveStorage.previewers.include?(previewer)
|
|
28
|
+
::ActiveStorage.previewers << previewer
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/exr_viewer.rb
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
|
|
6
|
+
module ExrViewer
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :strategy,
|
|
12
|
+
type: :string,
|
|
13
|
+
default: "auto",
|
|
14
|
+
desc: "Install strategy: auto, bun, node, importmap, vendored"
|
|
15
|
+
class_option :skip_install,
|
|
16
|
+
type: :boolean,
|
|
17
|
+
default: false,
|
|
18
|
+
desc: "Skip package manager install commands"
|
|
19
|
+
|
|
20
|
+
THREE_VERSION = "0.164.1"
|
|
21
|
+
THREE_PACKAGE = "three"
|
|
22
|
+
STIMULUS_PACKAGE = "@hotwired/stimulus"
|
|
23
|
+
|
|
24
|
+
IMPORTMAP_STIMULUS_PIN = %(pin "@hotwired/stimulus", to: "stimulus.min.js")
|
|
25
|
+
IMPORTMAP_STIMULUS_LOADING_PIN = %(pin "@hotwired/stimulus-loading", to: "stimulus-loading.js")
|
|
26
|
+
IMPORTMAP_CONTROLLERS_PIN = %(pin_all_from "app/javascript/controllers", under: "controllers")
|
|
27
|
+
IMPORTMAP_THREE_PIN = %(pin "three", to: "https://ga.jspm.io/npm:three@#{THREE_VERSION}/build/three.module.js")
|
|
28
|
+
IMPORTMAP_EXR_LOADER_PIN = %(pin "three/examples/jsm/loaders/EXRLoader.js", to: "https://ga.jspm.io/npm:three@#{THREE_VERSION}/examples/jsm/loaders/EXRLoader.js")
|
|
29
|
+
IMPORTMAP_WEBGPU_RENDERER_PIN = %(pin "three/examples/jsm/renderers/webgpu/WebGPURenderer.js", to: "https://ga.jspm.io/npm:three@#{THREE_VERSION}/examples/jsm/renderers/webgpu/WebGPURenderer.js")
|
|
30
|
+
|
|
31
|
+
def install
|
|
32
|
+
strategy = resolve_strategy
|
|
33
|
+
say_status :exr_viewer, "Using #{strategy} install strategy", :blue
|
|
34
|
+
|
|
35
|
+
case strategy
|
|
36
|
+
when "bun"
|
|
37
|
+
install_bun
|
|
38
|
+
when "node"
|
|
39
|
+
install_node
|
|
40
|
+
when "importmap"
|
|
41
|
+
install_importmap
|
|
42
|
+
when "vendored"
|
|
43
|
+
install_vendored
|
|
44
|
+
else
|
|
45
|
+
raise Thor::Error, "Unsupported strategy: #{strategy}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def resolve_strategy
|
|
52
|
+
selected = options[:strategy].to_s
|
|
53
|
+
selected = "auto" if selected.empty?
|
|
54
|
+
|
|
55
|
+
return detect_strategy if selected == "auto"
|
|
56
|
+
|
|
57
|
+
valid = %w[bun node importmap vendored]
|
|
58
|
+
unless valid.include?(selected)
|
|
59
|
+
raise Thor::Error, "Unknown --strategy=#{selected}. Use one of: auto, #{valid.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
selected
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def detect_strategy
|
|
66
|
+
return "importmap" if File.exist?(app_path("config/importmap.rb"))
|
|
67
|
+
return "bun" if bun_project?
|
|
68
|
+
return "node" if File.exist?(app_path("package.json"))
|
|
69
|
+
|
|
70
|
+
"vendored"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def bun_project?
|
|
74
|
+
File.exist?(app_path("bun.lockb")) || File.exist?(app_path("bun.lock")) || File.exist?(app_path("bunfig.toml"))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def install_bun
|
|
78
|
+
ensure_package_json!
|
|
79
|
+
|
|
80
|
+
if options[:skip_install]
|
|
81
|
+
add_package_dependencies([THREE_PACKAGE, STIMULUS_PACKAGE])
|
|
82
|
+
else
|
|
83
|
+
run "bun add #{THREE_PACKAGE} #{STIMULUS_PACKAGE}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
ensure_node_stimulus_setup
|
|
87
|
+
copy_controller_template
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def install_node
|
|
91
|
+
ensure_package_json!
|
|
92
|
+
|
|
93
|
+
if options[:skip_install]
|
|
94
|
+
add_package_dependencies([THREE_PACKAGE, STIMULUS_PACKAGE])
|
|
95
|
+
else
|
|
96
|
+
run "#{node_add_command} #{THREE_PACKAGE} #{STIMULUS_PACKAGE}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
ensure_node_stimulus_setup
|
|
100
|
+
copy_controller_template
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def install_importmap
|
|
104
|
+
importmap_path = app_path("config/importmap.rb")
|
|
105
|
+
raise Thor::Error, "Missing config/importmap.rb for importmap strategy" unless File.exist?(importmap_path)
|
|
106
|
+
|
|
107
|
+
ensure_importmap_stimulus_setup(importmap_path)
|
|
108
|
+
copy_controller_template
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def install_vendored
|
|
112
|
+
ensure_layout_include_tag
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def ensure_package_json!
|
|
116
|
+
return if File.exist?(app_path("package.json"))
|
|
117
|
+
|
|
118
|
+
raise Thor::Error,
|
|
119
|
+
"Missing package.json. Run with --strategy=importmap or --strategy=vendored, or initialize JavaScript tooling first."
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def add_package_dependencies(packages)
|
|
123
|
+
package_json_path = app_path("package.json")
|
|
124
|
+
package_json = JSON.parse(File.read(package_json_path))
|
|
125
|
+
package_json["dependencies"] ||= {}
|
|
126
|
+
|
|
127
|
+
packages.each do |package_name|
|
|
128
|
+
next if package_json["dependencies"].key?(package_name)
|
|
129
|
+
|
|
130
|
+
package_json["dependencies"][package_name] = package_name == THREE_PACKAGE ? "^#{THREE_VERSION}" : "^3.2.2"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def node_add_command
|
|
137
|
+
return "yarn add" if File.exist?(app_path("yarn.lock"))
|
|
138
|
+
return "pnpm add" if File.exist?(app_path("pnpm-lock.yaml"))
|
|
139
|
+
|
|
140
|
+
"npm install"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def ensure_node_stimulus_setup
|
|
144
|
+
ensure_controllers_directory
|
|
145
|
+
ensure_controllers_application
|
|
146
|
+
ensure_node_controllers_index
|
|
147
|
+
ensure_application_import(%(import "./controllers";))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def ensure_importmap_stimulus_setup(importmap_path)
|
|
151
|
+
ensure_controllers_directory
|
|
152
|
+
ensure_controllers_application
|
|
153
|
+
ensure_importmap_controllers_index
|
|
154
|
+
ensure_application_import(%(import "controllers";))
|
|
155
|
+
|
|
156
|
+
append_line_unless_present(importmap_path, IMPORTMAP_STIMULUS_PIN)
|
|
157
|
+
append_line_unless_present(importmap_path, IMPORTMAP_STIMULUS_LOADING_PIN)
|
|
158
|
+
append_line_unless_present(importmap_path, IMPORTMAP_CONTROLLERS_PIN)
|
|
159
|
+
append_line_unless_present(importmap_path, IMPORTMAP_THREE_PIN)
|
|
160
|
+
append_line_unless_present(importmap_path, IMPORTMAP_EXR_LOADER_PIN)
|
|
161
|
+
append_line_unless_present(importmap_path, IMPORTMAP_WEBGPU_RENDERER_PIN)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def ensure_controllers_directory
|
|
165
|
+
empty_directory app_path("app/javascript/controllers")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def ensure_controllers_application
|
|
169
|
+
application_path = app_path("app/javascript/controllers/application.js")
|
|
170
|
+
return if File.exist?(application_path)
|
|
171
|
+
|
|
172
|
+
copy_file "controllers_application.js", application_path
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def ensure_node_controllers_index
|
|
176
|
+
index_path = app_path("app/javascript/controllers/index.js")
|
|
177
|
+
unless File.exist?(index_path)
|
|
178
|
+
copy_file "controllers_index_for_node.js", index_path
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
unless File.read(index_path).include?("import { application }")
|
|
182
|
+
append_line_unless_present(index_path, %(import { application } from "./application"))
|
|
183
|
+
end
|
|
184
|
+
append_line_unless_present(index_path, %(import ExrViewerController from "./exr_viewer_controller"))
|
|
185
|
+
append_line_unless_present(index_path, %(application.register("exr-viewer", ExrViewerController)))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def ensure_importmap_controllers_index
|
|
189
|
+
index_path = app_path("app/javascript/controllers/index.js")
|
|
190
|
+
return if File.exist?(index_path)
|
|
191
|
+
|
|
192
|
+
copy_file "controllers_index_for_importmap.js", index_path
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def copy_controller_template
|
|
196
|
+
controller_path = app_path("app/javascript/controllers/exr_viewer_controller.js")
|
|
197
|
+
return if File.exist?(controller_path)
|
|
198
|
+
|
|
199
|
+
copy_file "exr_viewer_controller.js", controller_path
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def ensure_application_import(import_line)
|
|
203
|
+
application_js_path = app_path("app/javascript/application.js")
|
|
204
|
+
unless File.exist?(application_js_path)
|
|
205
|
+
empty_directory app_path("app/javascript")
|
|
206
|
+
File.write(application_js_path, "")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
append_line_unless_present(application_js_path, import_line)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def ensure_layout_include_tag
|
|
213
|
+
layout_path = app_path("app/views/layouts/application.html.erb")
|
|
214
|
+
unless File.exist?(layout_path)
|
|
215
|
+
say_status :warn, "No app/views/layouts/application.html.erb found. Add <%= exr_viewer_include_tag %> to your layout.", :yellow
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
layout = File.read(layout_path)
|
|
220
|
+
return if layout.include?("<%= exr_viewer_include_tag")
|
|
221
|
+
|
|
222
|
+
if layout.include?("</head>")
|
|
223
|
+
updated = layout.sub("</head>", " <%= exr_viewer_include_tag %>\n</head>")
|
|
224
|
+
File.write(layout_path, updated)
|
|
225
|
+
else
|
|
226
|
+
File.write(layout_path, "#{layout}\n<%= exr_viewer_include_tag %>\n")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def append_line_unless_present(path, line)
|
|
231
|
+
contents = File.read(path)
|
|
232
|
+
return if contents.include?(line)
|
|
233
|
+
|
|
234
|
+
separator = contents.end_with?("\n") || contents.empty? ? "" : "\n"
|
|
235
|
+
File.write(path, "#{contents}#{separator}#{line}\n")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def app_path(relative_path)
|
|
239
|
+
File.expand_path(relative_path, destination_root)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { application } from "./application"
|