From 71eea6c77f47b004b0d51cc07a8dccf75aa1ae24 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 8 Jun 2026 10:14:46 +0100 Subject: [PATCH 01/25] add f3d NPM package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2748066a3..fa7f831c2 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "bootswatch": "^5.3.8", "comlink": "^4.4.2", "driver.js": "^1.8.0", + "f3d": "^3.5.0", "i18n-js": "^4.5.3", "masonry-layout": "^4.2.2", "three": "^0.186.1", From 65ea43c000299b4b247c29ed1497a8fea735ec33 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 8 Jun 2026 11:13:11 +0100 Subject: [PATCH 02/25] add unsafe-eval script src to allow WASM loading Not ideal, will look at a better way later. --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dfd9e16af..6dcb86ec7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -109,7 +109,7 @@ class ApplicationController < ActionController::Base content_security_policy.img_src(*img_src) content_security_policy.worker_src :self, :blob content_security_policy.object_src :none - content_security_policy.script_src :self + content_security_policy.script_src :self, :unsafe_eval content_security_policy.style_src :self content_security_policy.style_src_attr :unsafe_inline content_security_policy.style_src_elem :self, "https://fonts.googleapis.com" From 9c528574c07a4a4fb1462a03a4c49eedd5db5033 Mon Sep 17 00:00:00 2001 From: James Smith Date: Tue, 9 Jun 2026 17:25:36 +0100 Subject: [PATCH 03/25] Add stub F3D WASM renderer --- app/components/renderers/f3d.rb | 51 +++++++++++++++++++ .../controllers/f3d_renderer_controller.ts | 41 +++++++++++++++ app/frontend/javascript/controllers/index.ts | 2 + app/lib/file_handlers/f3d.rb | 6 ++- 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 app/components/renderers/f3d.rb create mode 100644 app/frontend/javascript/controllers/f3d_renderer_controller.ts diff --git a/app/components/renderers/f3d.rb b/app/components/renderers/f3d.rb new file mode 100644 index 000000000..110e6c89c --- /dev/null +++ b/app/components/renderers/f3d.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class Components::Renderers::F3d < Components::Renderers::Base + include Phlex::Rails::Helpers::NumberToHumanSize + + def self.supports?(file) + FileHandlers::F3d.can_load? file&.mime_type + end + + def before_template + @settings = current_user&.renderer_settings || SiteSettings::UserDefaults::RENDERER + @loading_bar_position_classes = @file.has_render? ? + "btn-sm mt-3 translate-middle-x translate-top" : + "top-50 translate-middle" + end + + def view_template + div class: "position-relative", data: {turbo_permanent: true} do + img src: model_model_file_path(@file.model, @file, format: @file.extension, derivative: :render), class: "card-img-top image-preview", alt: @file.name if @file.has_render? + canvas id: "canvas", + class: "object-preview position-relative", + tabindex: "0", + data: { + controller: "f3d-renderer", + preview_url: model_model_file_raw_path(@file.model, @file.filename), + format: @file.extension, + y_up: @file.y_up.to_s, + grid_size_x: @settings["grid_width"], + grid_size_z: @settings["grid_depth"], + show_grid: @settings["show_grid"].to_s, + enable_pan_zoom: @settings["enable_pan_zoom"].to_s, + background_colour: @settings["background_colour"], + object_colour: @settings["object_colour"], + render_style: @settings["render_style"], + auto_load: ((@file.size || 9_999_999.megabytes) < (@settings["auto_load_max_size"] || 9_999_999).megabytes) ? "true" : "false", + action: "click->f3d-renderer#onLoad" + } + div class: "p-0 btn btn-secondary load-progress object-preview-progress position-absolute start-50 #{@loading_bar_position_classes}", role: "presentation" do + div class: "progress-bar bg-info progress-bar-animated progress-bar-striped", role: "progressbar", style: "width: 0%; height: 100%", + aria_label: "Loading progress", aria_valuenow: "0", aria_valuemin: "0", aria_valuemax: "100" + span class: "progress-label position-absolute top-50 start-50 translate-middle", role: "button", data: {action: "click->f3d-renderer#onLoad"} do + Icon icon: "box" + whitespace + span { t("renderer.load") } + whitespace + span { "(#{number_to_human_size @file.size, precision: 2})" } + end + end + end + end +end diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts new file mode 100644 index 000000000..0d6e1d37b --- /dev/null +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -0,0 +1,41 @@ +import { Controller } from '@hotwired/stimulus' +import f3d from 'f3d' + +// Connects to data-controller="f3d-renderer" +export default class extends Controller { + onLoad (): void { + const settings = { + canvas: this.element, + setupOptions: (options) => { + // background must be set to black for proper blending with transparent canvas + options.setAsString("render.background.color", "#000000"); + + // make it look nice + options.toggle("render.effect.antialiasing.enable"); + options.toggle("render.effect.tone_mapping"); + options.toggle("render.effect.ambient_occlusion"); + options.toggle("render.hdri.ambient"); + + // display widgets + options.toggle("ui.axis", true); + options.toggle("render.grid.enable", true); + + // default to +Z + options.setAsString("scene.up_direction", "+Z"); + } + }; + f3d(settings).then(async (F3D) => { + F3D.Engine.autoloadPlugins(); + F3D.engineInstance = F3D.Engine.create(); + F3D.setupOptions(F3D.engineInstance.getOptions()); + const canvas = this.element; + const scale = window.devicePixelRatio; + F3D.engineInstance + .getWindow() + .setSize(scale * canvas.clientWidth, scale * canvas.clientHeight); + // do a first render and start the interactor + F3D.engineInstance.getWindow().render(); + F3D.engineInstance.getInteractor().start(); + }) + } +} diff --git a/app/frontend/javascript/controllers/index.ts b/app/frontend/javascript/controllers/index.ts index 07b58f469..8a48aa71c 100644 --- a/app/frontend/javascript/controllers/index.ts +++ b/app/frontend/javascript/controllers/index.ts @@ -11,6 +11,7 @@ import CocoonedController from './cocooned_controller' import CollectionInputController from './collection_input_controller' import CopyTextController from './copy_text_controller' import EditableController from './editable_controller' +import F3dRendererController from './f3d_renderer_controller' import I18nController from './i18n_controller' import ModelSelectController from './model_select_controller' import ObfuscatedTextController from './obfuscated_text_controller' @@ -30,6 +31,7 @@ application.register('cocooned', CocoonedController) application.register('collection-input', CollectionInputController) application.register('copy-text', CopyTextController) application.register('editable', EditableController) +application.register('f3d-renderer', F3dRendererController) application.register('i18n', I18nController) application.register('model-select', ModelSelectController) application.register('obfuscated-text', ObfuscatedTextController) diff --git a/app/lib/file_handlers/f3d.rb b/app/lib/file_handlers/f3d.rb index 8f424a2ba..6855b4112 100644 --- a/app/lib/file_handlers/f3d.rb +++ b/app/lib/file_handlers/f3d.rb @@ -3,10 +3,14 @@ class FileHandlers::F3d < FileHandlers::Base `f3d --list-readers` end - ENVIRONMENTS = [:server].freeze + ENVIRONMENTS = [:browser, :preview_frame, :server].freeze INPUT_TYPES = readers.lines .filter_map { it.match(/\w[a-z]*\/[0-9a-z.+-]*\w/)&.to_s } .filter_map { Mime::Type.lookup(it) } .uniq .freeze + + def self.component + Components::Renderers::F3d + end end From 2c373f39f52c592dad41792d2d088f862920141e Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 10 Jun 2026 10:48:49 +0100 Subject: [PATCH 04/25] load and render file - working! --- .../controllers/f3d_renderer_controller.ts | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 0d6e1d37b..e69f04d90 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -3,12 +3,12 @@ import f3d from 'f3d' // Connects to data-controller="f3d-renderer" export default class extends Controller { - onLoad (): void { + init (data: Uint8Array): void { const settings = { canvas: this.element, setupOptions: (options) => { // background must be set to black for proper blending with transparent canvas - options.setAsString("render.background.color", "#000000"); + options.setAsString("render.background.color", "#555555"); // make it look nice options.toggle("render.effect.antialiasing.enable"); @@ -33,9 +33,32 @@ export default class extends Controller { F3D.engineInstance .getWindow() .setSize(scale * canvas.clientWidth, scale * canvas.clientHeight); - // do a first render and start the interactor + + const scene = F3D.engineInstance.getScene(); + scene.clear(); + try { + scene.addBuffer(data) + } catch (e) { + console.log('Unsupported file') + } + F3D.engineInstance.getWindow().getCamera().resetToBounds(0.9); F3D.engineInstance.getWindow().render(); F3D.engineInstance.getInteractor().start(); }) } + + async onLoad () { + const url = this.element.dataset.previewUrl; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const result = await response.bytes(); + this.init(result); + } catch (error) { + console.error(error.message); + } + } } From d509793547faa7039f054e8ba252362f5cd7eda4 Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 10 Jun 2026 12:47:04 +0100 Subject: [PATCH 05/25] More accurate rendering and camera position --- .../controllers/f3d_renderer_controller.ts | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index e69f04d90..8ad32623b 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -3,47 +3,61 @@ import f3d from 'f3d' // Connects to data-controller="f3d-renderer" export default class extends Controller { - init (data: Uint8Array): void { + + engine: any; + + async init (data: Uint8Array): Promise { const settings = { canvas: this.element, - setupOptions: (options) => { - // background must be set to black for proper blending with transparent canvas - options.setAsString("render.background.color", "#555555"); - - // make it look nice - options.toggle("render.effect.antialiasing.enable"); - options.toggle("render.effect.tone_mapping"); - options.toggle("render.effect.ambient_occlusion"); - options.toggle("render.hdri.ambient"); - - // display widgets - options.toggle("ui.axis", true); - options.toggle("render.grid.enable", true); - - // default to +Z - options.setAsString("scene.up_direction", "+Z"); - } }; f3d(settings).then(async (F3D) => { F3D.Engine.autoloadPlugins(); - F3D.engineInstance = F3D.Engine.create(); - F3D.setupOptions(F3D.engineInstance.getOptions()); - const canvas = this.element; - const scale = window.devicePixelRatio; - F3D.engineInstance - .getWindow() - .setSize(scale * canvas.clientWidth, scale * canvas.clientHeight); + this.engine = F3D.Engine.create(); + const options = this.engine.getOptions(); + // background must be set to black for proper blending with transparent canvas + options.setAsString("render.background.color", "#555555"); - const scene = F3D.engineInstance.getScene(); - scene.clear(); + // make it look nice + options.toggle('render.effect.antialiasing.enable') + options.toggle('render.effect.tone_mapping') + options.toggle('render.effect.ambient_occlusion') + + options.toggle('scene.animation.autoplay', true) + + // display widgets + options.toggle('render.grid.enable', true) + options.setAsString('render.grid.color', '#00ffff') + options.setAsString('render.grid.subdivisions', '0') + options.setAsString('render.grid.unit', '10') + + // default to +Z + options.setAsString('scene.up_direction', '+Z') + + const canvas = this.element + const scale = window.devicePixelRatio + console.log(scale * canvas.clientWidth) + console.log(scale * canvas.clientHeight) + this.engine.getWindow().setSize(scale * canvas.clientWidth, scale * canvas.clientHeight) + + const scene = this.engine.getScene() + scene.clear() try { scene.addBuffer(data) } catch (e) { console.log('Unsupported file') } - F3D.engineInstance.getWindow().getCamera().resetToBounds(0.9); - F3D.engineInstance.getWindow().render(); - F3D.engineInstance.getInteractor().start(); + + const camera = this.engine.getWindow().getCamera() + const foc = camera.focalPoint + const dir = [-1, 1, -0.5] + const pos = [0, 0, 0] + for (let i = 0; i < 3; i++) { + pos[i] = foc[i] - dir[i] + } + camera.position = pos + camera.resetToBounds(0.9) + this.engine.getWindow().render() + this.engine.getInteractor().start() }) } From 5a7d9faaaf3805e43a499abe7d13afaeec8af70f Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 11:30:38 +0100 Subject: [PATCH 06/25] fix background blending --- app/frontend/javascript/controllers/f3d_renderer_controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 8ad32623b..5578bd705 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -15,7 +15,7 @@ export default class extends Controller { this.engine = F3D.Engine.create(); const options = this.engine.getOptions(); // background must be set to black for proper blending with transparent canvas - options.setAsString("render.background.color", "#555555"); + options.setAsString('render.background.color', '#000000') // make it look nice options.toggle('render.effect.antialiasing.enable') From 1360420838e0c449e389cba34ae2c94acfc05af4 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 11:49:39 +0100 Subject: [PATCH 07/25] split f3d handler into different WASM and CLI versions --- app/components/renderers/f3d.rb | 2 +- app/jobs/upgrade/backfill_model_renders.rb | 2 +- app/lib/file_handlers/{f3d.rb => f3d_cli.rb} | 8 +--- app/lib/file_handlers/f3d_wasm.rb | 46 +++++++++++++++++++ app/uploaders/application_uploader.rb | 2 +- .../{f3d_spec.rb => f3d_cli_spec.rb} | 2 +- spec/support/f3d.rb | 2 +- 7 files changed, 53 insertions(+), 11 deletions(-) rename app/lib/file_handlers/{f3d.rb => f3d_cli.rb} (56%) create mode 100644 app/lib/file_handlers/f3d_wasm.rb rename spec/lib/file_handlers/{f3d_spec.rb => f3d_cli_spec.rb} (72%) diff --git a/app/components/renderers/f3d.rb b/app/components/renderers/f3d.rb index 110e6c89c..0a455fb3d 100644 --- a/app/components/renderers/f3d.rb +++ b/app/components/renderers/f3d.rb @@ -4,7 +4,7 @@ class Components::Renderers::F3d < Components::Renderers::Base include Phlex::Rails::Helpers::NumberToHumanSize def self.supports?(file) - FileHandlers::F3d.can_load? file&.mime_type + FileHandlers::F3dWasm.can_load? file&.mime_type end def before_template diff --git a/app/jobs/upgrade/backfill_model_renders.rb b/app/jobs/upgrade/backfill_model_renders.rb index e28bdcdcb..2f2023f63 100644 --- a/app/jobs/upgrade/backfill_model_renders.rb +++ b/app/jobs/upgrade/backfill_model_renders.rb @@ -2,7 +2,7 @@ class Upgrade::BackfillModelRenders < Upgrade::BackfillDerivativesBase queue_as :performance def mime_types - FileHandlers::F3d.input_types.without(MediaType.image_types) + FileHandlers::F3dCli.input_types.without(MediaType.image_types) end def derivative diff --git a/app/lib/file_handlers/f3d.rb b/app/lib/file_handlers/f3d_cli.rb similarity index 56% rename from app/lib/file_handlers/f3d.rb rename to app/lib/file_handlers/f3d_cli.rb index 6855b4112..babdff974 100644 --- a/app/lib/file_handlers/f3d.rb +++ b/app/lib/file_handlers/f3d_cli.rb @@ -1,16 +1,12 @@ -class FileHandlers::F3d < FileHandlers::Base +class FileHandlers::F3dCli < FileHandlers::Base def self.readers `f3d --list-readers` end - ENVIRONMENTS = [:browser, :preview_frame, :server].freeze + ENVIRONMENTS = [:server].freeze INPUT_TYPES = readers.lines .filter_map { it.match(/\w[a-z]*\/[0-9a-z.+-]*\w/)&.to_s } .filter_map { Mime::Type.lookup(it) } .uniq .freeze - - def self.component - Components::Renderers::F3d - end end diff --git a/app/lib/file_handlers/f3d_wasm.rb b/app/lib/file_handlers/f3d_wasm.rb new file mode 100644 index 000000000..2b21830e3 --- /dev/null +++ b/app/lib/file_handlers/f3d_wasm.rb @@ -0,0 +1,46 @@ +class FileHandlers::F3dWasm < FileHandlers::Base + ENVIRONMENTS = [:browser, :preview_frame].freeze + INPUT_TYPES = Mime::EXTENSION_LOOKUP.slice( + "gml", + "gltf", + "glb", + "obj", + "ply", + "pts", + "stl", + "vtk", + "vtp", + "vtu", + "vtkhdf", + "3ds", + "wrl", + "vrml", + "fbx", + "off", + "x", + "dae", + "ex2", + "exo", + "e", + "g", + "stp", + "step", + "igs", + "iges", + "brep", + "xbf", + "drc", + "mdl", + "nc", + "cdf", + "ncdf" + ).values.freeze + + def self.priority + 100 + end + + def self.component + Components::Renderers::F3d + end +end diff --git a/app/uploaders/application_uploader.rb b/app/uploaders/application_uploader.rb index e50972199..d84c9aae4 100644 --- a/app/uploaders/application_uploader.rb +++ b/app/uploaders/application_uploader.rb @@ -141,7 +141,7 @@ class ApplicationUploader < Shrine Shrine.with_file(original) do {render: FreecadThumbnailExtractorService.new(file: it).call}.compact end - elsif SiteSettings.generate_model_renders && FileHandlers::F3d.can_load?(context[:record].mime_type) && context[:record]&.is_3d_model? + elsif SiteSettings.generate_model_renders && FileHandlers::F3dCli.can_load?(context[:record].mime_type) && context[:record]&.is_3d_model? Shrine.with_file(original) do up = context[:record]&.up_direction options = F3D_OPTS.merge( diff --git a/spec/lib/file_handlers/f3d_spec.rb b/spec/lib/file_handlers/f3d_cli_spec.rb similarity index 72% rename from spec/lib/file_handlers/f3d_spec.rb rename to spec/lib/file_handlers/f3d_cli_spec.rb index 7b2925d50..3fad30955 100644 --- a/spec/lib/file_handlers/f3d_spec.rb +++ b/spec/lib/file_handlers/f3d_cli_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe FileHandlers::F3d do +RSpec.describe FileHandlers::F3dCli do it "can execute f3d" do expect(`f3d --version`).to include "F3D" end diff --git a/spec/support/f3d.rb b/spec/support/f3d.rb index a57525b6e..e1119df47 100644 --- a/spec/support/f3d.rb +++ b/spec/support/f3d.rb @@ -1,6 +1,6 @@ RSpec.configure do |config| config.before do - allow(FileHandlers::F3d).to receive(:input_types).and_return([ + allow(FileHandlers::F3dCli).to receive(:input_types).and_return([ Mime[:threemf], Mime[:obj], Mime[:stl], Mime[:jpg] ]) end From 162e1aafd9acd9f47a6db4717beca9cfe371e5cd Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 11:51:05 +0100 Subject: [PATCH 08/25] clear file handler cache keys on startup --- config/initializers/cache_clearance.rb | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 config/initializers/cache_clearance.rb diff --git a/config/initializers/cache_clearance.rb b/config/initializers/cache_clearance.rb new file mode 100644 index 000000000..4120198aa --- /dev/null +++ b/config/initializers/cache_clearance.rb @@ -0,0 +1,4 @@ +Rails.application.config.after_initialize do + Rails.cache.delete_matched("FileHandlers_handlers_for_*") +rescue RedisClient::CannotConnectError +end From 250da78135e7ea299353273635bfd0b78c996774 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 13:09:34 +0100 Subject: [PATCH 09/25] update F3D WASM file support list --- app/lib/file_handlers/f3d_wasm.rb | 36 +------------------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/app/lib/file_handlers/f3d_wasm.rb b/app/lib/file_handlers/f3d_wasm.rb index 2b21830e3..d6fa0a8be 100644 --- a/app/lib/file_handlers/f3d_wasm.rb +++ b/app/lib/file_handlers/f3d_wasm.rb @@ -1,40 +1,6 @@ class FileHandlers::F3dWasm < FileHandlers::Base ENVIRONMENTS = [:browser, :preview_frame].freeze - INPUT_TYPES = Mime::EXTENSION_LOOKUP.slice( - "gml", - "gltf", - "glb", - "obj", - "ply", - "pts", - "stl", - "vtk", - "vtp", - "vtu", - "vtkhdf", - "3ds", - "wrl", - "vrml", - "fbx", - "off", - "x", - "dae", - "ex2", - "exo", - "e", - "g", - "stp", - "step", - "igs", - "iges", - "brep", - "xbf", - "drc", - "mdl", - "nc", - "cdf", - "ncdf" - ).values.freeze + INPUT_TYPES = Mime::EXTENSION_LOOKUP.slice(*%w[gml gltf glb obj ply pts stl vtk vtp vtu vtkhdf 3ds wrl vrml fbx dae dxf off x 3mf ex2 exo e g stp step igs iges brep xbf drc mdl nc cdf ncdf ifc]).values.uniq.freeze def self.priority 100 From 9d7d144cac54459cd44ee66bc82ccd0e34fffd35 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 13:14:25 +0100 Subject: [PATCH 10/25] Add dev line for getting list of F3D formats --- .../javascript/controllers/f3d_renderer_controller.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 5578bd705..93e15c7e8 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -12,8 +12,10 @@ export default class extends Controller { }; f3d(settings).then(async (F3D) => { F3D.Engine.autoloadPlugins(); - this.engine = F3D.Engine.create(); - const options = this.engine.getOptions(); + // Uncomment this to get an updated list of formats in dev + // console.log(`F3D supported types: ${F3D.Engine.getReadersInfo().map((reader) => (`${reader["extensions"][0]}: ${reader["mimeTypes"][0]}`)).flat()}`); + this.engine = F3D.Engine.create() + const options = this.engine.getOptions() // background must be set to black for proper blending with transparent canvas options.setAsString('render.background.color', '#000000') From 01bfd1166c8018bbd26e91dbf001c3dee995f933 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 13:14:49 +0100 Subject: [PATCH 11/25] check URL before loading and add type safety --- .../controllers/f3d_renderer_controller.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 93e15c7e8..2b600647a 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -64,17 +64,19 @@ export default class extends Controller { } async onLoad () { - const url = this.element.dataset.previewUrl; - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); - } + const url = (this.element as HTMLCanvasElement).dataset.previewUrl; + if (url) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } - const result = await response.bytes(); - this.init(result); - } catch (error) { - console.error(error.message); + const result = await response.bytes(); + this.init(result); + } catch (error) { + console.error(error.message); + } } } } From 457cbf6c229c1c20c89fcb113d54dc7831db3223 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 16:55:34 +0100 Subject: [PATCH 12/25] move load command back onto loading bar --- app/components/renderers/f3d.rb | 5 ++--- .../controllers/f3d_renderer_controller.ts | 14 +++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/components/renderers/f3d.rb b/app/components/renderers/f3d.rb index 0a455fb3d..59efd1bf2 100644 --- a/app/components/renderers/f3d.rb +++ b/app/components/renderers/f3d.rb @@ -32,13 +32,12 @@ class Components::Renderers::F3d < Components::Renderers::Base background_colour: @settings["background_colour"], object_colour: @settings["object_colour"], render_style: @settings["render_style"], - auto_load: ((@file.size || 9_999_999.megabytes) < (@settings["auto_load_max_size"] || 9_999_999).megabytes) ? "true" : "false", - action: "click->f3d-renderer#onLoad" + auto_load: ((@file.size || 9_999_999.megabytes) < (@settings["auto_load_max_size"] || 9_999_999).megabytes) ? "true" : "false" } div class: "p-0 btn btn-secondary load-progress object-preview-progress position-absolute start-50 #{@loading_bar_position_classes}", role: "presentation" do div class: "progress-bar bg-info progress-bar-animated progress-bar-striped", role: "progressbar", style: "width: 0%; height: 100%", aria_label: "Loading progress", aria_valuenow: "0", aria_valuemin: "0", aria_valuemax: "100" - span class: "progress-label position-absolute top-50 start-50 translate-middle", role: "button", data: {action: "click->f3d-renderer#onLoad"} do + span class: "progress-label position-absolute top-50 start-50 translate-middle", role: "button" do Icon icon: "box" whitespace span { t("renderer.load") } diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 2b600647a..93a884433 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -3,8 +3,16 @@ import f3d from 'f3d' // Connects to data-controller="f3d-renderer" export default class extends Controller { + engine: any + progressBar: HTMLDivElement | null + progressLabel: HTMLSpanElement | null - engine: any; + connect (): void { + this.progressBar = this.element.parentElement?.getElementsByClassName('progress-bar')[0] as HTMLDivElement + this.progressLabel = this.element.parentElement?.getElementsByClassName('progress-label')[0] as HTMLSpanElement + const loadButton = this.element.parentElement?.getElementsByClassName('object-preview-progress')[0] as HTMLDivElement + loadButton.addEventListener('click', this.load.bind(this)) + } async init (data: Uint8Array): Promise { const settings = { @@ -63,8 +71,8 @@ export default class extends Controller { }) } - async onLoad () { - const url = (this.element as HTMLCanvasElement).dataset.previewUrl; + async load (): Promise { + const url = (this.element as HTMLCanvasElement).dataset.previewUrl if (url) { try { const response = await fetch(url); From f4223e00ade2f532faed9d3debb7deeb9d7cd99a Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 18:06:55 +0100 Subject: [PATCH 13/25] show load progress for f3d wasm loader --- .../controllers/f3d_renderer_controller.ts | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 93a884433..5e7ebaaff 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -19,7 +19,11 @@ export default class extends Controller { canvas: this.element, }; f3d(settings).then(async (F3D) => { - F3D.Engine.autoloadPlugins(); + this.progressBar?.parentElement?.remove() + this.progressBar = null + this.progressLabel = null + + F3D.Engine.autoloadPlugins() // Uncomment this to get an updated list of formats in dev // console.log(`F3D supported types: ${F3D.Engine.getReadersInfo().map((reader) => (`${reader["extensions"][0]}: ${reader["mimeTypes"][0]}`)).flat()}`); this.engine = F3D.Engine.create() @@ -73,18 +77,37 @@ export default class extends Controller { async load (): Promise { const url = (this.element as HTMLCanvasElement).dataset.previewUrl - if (url) { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); + if (url != null) { + const xhr = new XMLHttpRequest() + xhr.open('GET', url) + xhr.responseType = 'arraybuffer' + xhr.addEventListener('progress', this.onLoadProgress.bind(this)) + xhr.addEventListener('error', this.onLoadError.bind(this)) + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + this.init(new Uint8Array(xhr.response)) } - - const result = await response.bytes(); - this.init(result); - } catch (error) { - console.error(error.message); - } + }.bind(this) + xhr.send() } } + + onLoadProgress (event): void { + const percentage = Math.round((event.loaded / event.total) * 100) + if ((this.progressBar == null) || (this.progressLabel == null)) { return } + if (percentage === 100) { + this.progressLabel.textContent = window.i18n.t('renderer.processing') // i18n-tasks-use t('renderer.processing') + } else { + this.progressLabel.textContent = `${percentage}%` + } + this.progressBar.style.width = `${percentage}%` + this.progressBar.ariaValueNow = percentage.toString() + } + + onLoadError (): void { + if ((this.progressBar == null) || (this.progressLabel == null)) { return } + this.progressBar.classList.add('bg-danger') + this.progressBar.style.width = this.progressBar.ariaValueNow = '100%' + this.progressLabel.textContent = window.i18n.t('renderer.errors.load') // i18n-tasks-use t('renderer.errors.load') + } } From 4fb902d5d93436684d4b60a7a55d262096abc4fa Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 12 Jun 2026 10:30:30 +0100 Subject: [PATCH 14/25] lint --- .../javascript/controllers/f3d_renderer_controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 5e7ebaaff..35f5ce652 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -16,9 +16,9 @@ export default class extends Controller { async init (data: Uint8Array): Promise { const settings = { - canvas: this.element, - }; - f3d(settings).then(async (F3D) => { + canvas: this.element + } + await f3d(settings).then(async (F3D) => { this.progressBar?.parentElement?.remove() this.progressBar = null this.progressLabel = null From 04910ac95ef21921f0fec3ebefb77b794fb7ce87 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 12:04:21 +0100 Subject: [PATCH 15/25] Fix render options for f3d --- .../javascript/controllers/f3d_renderer_controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 35f5ce652..f799c3590 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -32,14 +32,14 @@ export default class extends Controller { options.setAsString('render.background.color', '#000000') // make it look nice - options.toggle('render.effect.antialiasing.enable') + // options.setAsString('render.effect.antialiasing.enable', 'true') options.toggle('render.effect.tone_mapping') options.toggle('render.effect.ambient_occlusion') - options.toggle('scene.animation.autoplay', true) + options.toggle('scene.animation.autoplay') // display widgets - options.toggle('render.grid.enable', true) + options.toggle('render.grid.enable') options.setAsString('render.grid.color', '#00ffff') options.setAsString('render.grid.subdivisions', '0') options.setAsString('render.grid.unit', '10') From e4b24e4b0109e2a5967e6d8b1e5d47e070b7b081 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 12:04:39 +0100 Subject: [PATCH 16/25] clear canvas background when 3d renderer is ready --- .../javascript/controllers/f3d_renderer_controller.ts | 1 + yarn.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index f799c3590..f9ffdfa42 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -70,6 +70,7 @@ export default class extends Controller { } camera.position = pos camera.resetToBounds(0.9) + canvas.style.backgroundColor = 'black' this.engine.getWindow().render() this.engine.getInteractor().start() }) diff --git a/yarn.lock b/yarn.lock index 5ba252ffb..0c3e56850 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2877,6 +2877,13 @@ __metadata: languageName: node linkType: hard +"f3d@npm:3.5.0-nightly.20261003": + version: 3.5.0-nightly.20261003 + resolution: "f3d@npm:3.5.0-nightly.20261003" + checksum: d3f3419feb1585953e33d9c8b423cdc3fe25f4cd5b1acc3cfa39ede28f411bbd5aa928f2f481bca067a1f58eb0b48c0eaad26ed955244a4003970763209f1e6b + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -4079,6 +4086,7 @@ __metadata: comlink: ^4.4.2 driver.js: ^1.8.0 eslint: ^9.0.0 + f3d: 3.5.0-nightly.20261003 husky: ^9.1.7 i18n-js: ^4.5.3 masonry-layout: ^4.2.2 From cc068860c41dc2f6b40d9a5b255654987f9b462d Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 12:04:54 +0100 Subject: [PATCH 17/25] Use f3d nightly release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa7f831c2..52736e2b5 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "bootswatch": "^5.3.8", "comlink": "^4.4.2", "driver.js": "^1.8.0", - "f3d": "^3.5.0", + "f3d": "3.5.0-nightly.20261003", "i18n-js": "^4.5.3", "masonry-layout": "^4.2.2", "three": "^0.186.1", From a55afd5792f30387a008d2d04fed26fbe51660a1 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 12:41:08 +0100 Subject: [PATCH 18/25] handle multiple f3d canvases per page --- app/components/renderers/f3d.rb | 2 +- app/frontend/javascript/controllers/f3d_renderer_controller.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/renderers/f3d.rb b/app/components/renderers/f3d.rb index 59efd1bf2..bdcee2a92 100644 --- a/app/components/renderers/f3d.rb +++ b/app/components/renderers/f3d.rb @@ -17,7 +17,7 @@ class Components::Renderers::F3d < Components::Renderers::Base def view_template div class: "position-relative", data: {turbo_permanent: true} do img src: model_model_file_path(@file.model, @file, format: @file.extension, derivative: :render), class: "card-img-top image-preview", alt: @file.name if @file.has_render? - canvas id: "canvas", + canvas id: "file-#{@file.to_param}", class: "object-preview position-relative", tabindex: "0", data: { diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index f9ffdfa42..c7812c0d9 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -26,7 +26,7 @@ export default class extends Controller { F3D.Engine.autoloadPlugins() // Uncomment this to get an updated list of formats in dev // console.log(`F3D supported types: ${F3D.Engine.getReadersInfo().map((reader) => (`${reader["extensions"][0]}: ${reader["mimeTypes"][0]}`)).flat()}`); - this.engine = F3D.Engine.create() + this.engine = F3D.Engine.create(`#${this.element.id}`) const options = this.engine.getOptions() // background must be set to black for proper blending with transparent canvas options.setAsString('render.background.color', '#000000') From c620a466b04fa407f4e4401b1f83593db0e53330 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 13:00:50 +0100 Subject: [PATCH 19/25] Add typing hint for canvas --- app/frontend/javascript/controllers/f3d_renderer_controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index c7812c0d9..711d89aea 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -47,7 +47,7 @@ export default class extends Controller { // default to +Z options.setAsString('scene.up_direction', '+Z') - const canvas = this.element + const canvas = this.element as HTMLCanvasElement const scale = window.devicePixelRatio console.log(scale * canvas.clientWidth) console.log(scale * canvas.clientHeight) From f6a5d95a1ee5239029d059c4baeb0eea4c0dcdff Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 15:46:49 +0100 Subject: [PATCH 20/25] Adjust camera for +y/+z models --- .../javascript/controllers/f3d_renderer_controller.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 711d89aea..253958b53 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -44,10 +44,10 @@ export default class extends Controller { options.setAsString('render.grid.subdivisions', '0') options.setAsString('render.grid.unit', '10') - // default to +Z - options.setAsString('scene.up_direction', '+Z') - const canvas = this.element as HTMLCanvasElement + + options.setAsString('scene.up_direction', canvas.dataset.yUp === 'true' ? '+Y' : '+Z') + const scale = window.devicePixelRatio console.log(scale * canvas.clientWidth) console.log(scale * canvas.clientHeight) @@ -63,7 +63,8 @@ export default class extends Controller { const camera = this.engine.getWindow().getCamera() const foc = camera.focalPoint - const dir = [-1, 1, -0.5] + + const dir = canvas.dataset.yUp === 'true' ? [-1, -0.5, -1] : [-1, 1, -0.5] const pos = [0, 0, 0] for (let i = 0; i < 3; i++) { pos[i] = foc[i] - dir[i] From 0b951af9fb35de50d4a3842072f5ea2f31c4f92d Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 15:47:13 +0100 Subject: [PATCH 21/25] Update renderer options and try to match CLI --- .../controllers/f3d_renderer_controller.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 253958b53..79d2b7369 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -28,21 +28,18 @@ export default class extends Controller { // console.log(`F3D supported types: ${F3D.Engine.getReadersInfo().map((reader) => (`${reader["extensions"][0]}: ${reader["mimeTypes"][0]}`)).flat()}`); this.engine = F3D.Engine.create(`#${this.element.id}`) const options = this.engine.getOptions() - // background must be set to black for proper blending with transparent canvas + + // Set renderer options the same as the CLI version + options.setAsString('render.effect.ambient_occlusion', 'true') + options.setAsString('render.effect.antialiasing.mode', 'fxaa') + options.setAsString('render.effect.blending.mode', 'ddp') options.setAsString('render.background.color', '#000000') - - // make it look nice - // options.setAsString('render.effect.antialiasing.enable', 'true') - options.toggle('render.effect.tone_mapping') - options.toggle('render.effect.ambient_occlusion') - - options.toggle('scene.animation.autoplay') - - // display widgets - options.toggle('render.grid.enable') + options.setAsString('render.grid.enable', 'true') options.setAsString('render.grid.color', '#00ffff') options.setAsString('render.grid.subdivisions', '0') options.setAsString('render.grid.unit', '10') + options.setAsString('render.effect.tone_mapping', 'true') + options.setAsString('scene.animation.autoplay', 'true') const canvas = this.element as HTMLCanvasElement From 850cb60609a30eb87689a687dfc87e4984f595e4 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 15:59:37 +0100 Subject: [PATCH 22/25] show load errors properly --- .../controllers/f3d_renderer_controller.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/frontend/javascript/controllers/f3d_renderer_controller.ts b/app/frontend/javascript/controllers/f3d_renderer_controller.ts index 79d2b7369..33ff78e9c 100644 --- a/app/frontend/javascript/controllers/f3d_renderer_controller.ts +++ b/app/frontend/javascript/controllers/f3d_renderer_controller.ts @@ -19,10 +19,6 @@ export default class extends Controller { canvas: this.element } await f3d(settings).then(async (F3D) => { - this.progressBar?.parentElement?.remove() - this.progressBar = null - this.progressLabel = null - F3D.Engine.autoloadPlugins() // Uncomment this to get an updated list of formats in dev // console.log(`F3D supported types: ${F3D.Engine.getReadersInfo().map((reader) => (`${reader["extensions"][0]}: ${reader["mimeTypes"][0]}`)).flat()}`); @@ -55,9 +51,20 @@ export default class extends Controller { try { scene.addBuffer(data) } catch (e) { - console.log('Unsupported file') + console.log(e) + if (this.progressBar && this.progressLabel) { + this.progressBar.classList.add('bg-danger') + this.progressBar.style.width = this.progressBar.ariaValueNow = '100%' + this.progressLabel.textContent = window.i18n.t('renderer.errors.load') // i18n-tasks-use t('renderer.errors.load') + } + return } + this.progressBar?.parentElement?.remove() + this.progressBar = null + this.progressLabel = null + canvas.style.backgroundColor = 'black' + const camera = this.engine.getWindow().getCamera() const foc = camera.focalPoint @@ -68,7 +75,6 @@ export default class extends Controller { } camera.position = pos camera.resetToBounds(0.9) - canvas.style.backgroundColor = 'black' this.engine.getWindow().render() this.engine.getInteractor().start() }) From 94c6236e0742acec762375056b1838543cf74a95 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 17:35:32 +0100 Subject: [PATCH 23/25] Prioritise three.js renderer over f3d, for now --- app/lib/file_handlers/f3d_wasm.rb | 2 +- app/lib/file_handlers/three.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/lib/file_handlers/f3d_wasm.rb b/app/lib/file_handlers/f3d_wasm.rb index d6fa0a8be..25a3b2bdf 100644 --- a/app/lib/file_handlers/f3d_wasm.rb +++ b/app/lib/file_handlers/f3d_wasm.rb @@ -3,7 +3,7 @@ class FileHandlers::F3dWasm < FileHandlers::Base INPUT_TYPES = Mime::EXTENSION_LOOKUP.slice(*%w[gml gltf glb obj ply pts stl vtk vtp vtu vtkhdf 3ds wrl vrml fbx dae dxf off x 3mf ex2 exo e g stp step igs iges brep xbf drc mdl nc cdf ncdf ifc]).values.uniq.freeze def self.priority - 100 + 1 end def self.component diff --git a/app/lib/file_handlers/three.rb b/app/lib/file_handlers/three.rb index 8d4b8f308..5d2c12e40 100644 --- a/app/lib/file_handlers/three.rb +++ b/app/lib/file_handlers/three.rb @@ -4,6 +4,10 @@ class FileHandlers::Three < FileHandlers::Base "stl", "obj", "3mf", "ply", "gltf", "glb", "drc", "fbx", "3ds", "gcode", "mpd", "ldr", "3dm" ).values.freeze + def self.priority + 100 + end + def self.component Components::Renderers::Three end From aee2110c49ea62dc5fd95a023755ee1653dc6f3d Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 5 Oct 2026 17:54:04 +0100 Subject: [PATCH 24/25] Missed an F3dCli change --- app/uploaders/application_uploader.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/uploaders/application_uploader.rb b/app/uploaders/application_uploader.rb index d84c9aae4..78825772b 100644 --- a/app/uploaders/application_uploader.rb +++ b/app/uploaders/application_uploader.rb @@ -90,7 +90,7 @@ class ApplicationUploader < Shrine end add_metadata :object do |io, context| - if context[:record]&.try(:is_3d_model?) && FileHandlers::F3d.can_load?(context[:record].mime_type) + if context[:record]&.try(:is_3d_model?) && FileHandlers::F3dCli.can_load?(context[:record].mime_type) bounds = Shrine.with_file(io) do |file| if file.path options = { From 3caffcbafaf822e6f5a23754a71cd81c2151ce3f Mon Sep 17 00:00:00 2001 From: James Smith Date: Tue, 6 Oct 2026 08:11:01 +0100 Subject: [PATCH 25/25] don't clear caches on precompile --- config/initializers/cache_clearance.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/cache_clearance.rb b/config/initializers/cache_clearance.rb index 4120198aa..2f0479ae7 100644 --- a/config/initializers/cache_clearance.rb +++ b/config/initializers/cache_clearance.rb @@ -1,4 +1,4 @@ Rails.application.config.after_initialize do - Rails.cache.delete_matched("FileHandlers_handlers_for_*") + Rails.cache.delete_matched("FileHandlers_handlers_for_*") if ENV["RAILS_ASSETS_PRECOMPILE"].blank? rescue RedisClient::CannotConnectError end