/* * TTSDeck.js * * Creates a deck image and corresponding "Saved Object" JSON for use * in Tabletop Simulator */ useLibrary("project"); useLibrary("imageutils"); useLibrary("threads"); useLibrary("uilayout"); useLibrary("uicontrols"); importClass(arkham.project.CopiesList); const TTSJson = require("./TTSJson.js"); const TTS_CARDS_PER_IMAGE = 69; const TTS_MAX_ROWS = 7; const getName = () => "TTSDeck"; const getDescription = () => "Generates a TTS deck image and JSON file"; const getVersion = () => 1.0; const getPluginType = () => arkham.plugins.Plugin.INJECTED; function unload() { unregisterAll(); } // Creates a test button during development that calls unload() to clean up. testProjectScript(); // TODO: allow setting a default copy count // Hack to override the default return value of 1 function copyCount(copies_list, name) { const entries = copies_list.getListEntries().map((x) => String(x)); if (entries.indexOf(String(name)) == -1) { return 1; } else { return copies_list.getCopyCount(name); } } function getImageFile(parent, format, page_num) { return new File( parent.file, parent.getName() + "_" + page_num + "." + format ); } function Card(member) { this.member = member; this.component = ResourceKit.getGameComponentFromFile(member.file); this.makeImageUncached = function makeImageUncached(resolution, back) { println("Generating image for card ", this.member); const sheets = this.component.createDefaultSheets(); const card_image = sheets[back ? 1 : 0].paint( arkham.sheet.RenderTarget.EXPORT, resolution ); return card_image; }; // export front face, or retrive it from a cached file // TODO: handle two-sided cards this.makeImage = function makeImage(format, resolution) { const cache_dir = new File(this.member.parent.file, ".ttsdeck_cache"); const cached_file = new File( cache_dir, this.member.file.name + "." + format ); if ( cached_file.exists() && cached_file.lastModified() > this.member.file.lastModified() ) { println("Got cached image for card", this.member); return ImageUtils.read(cached_file); } else { const card_image = this.makeImageUncached(resolution); cache_dir.mkdir(); ImageUtils.write(card_image, cached_file, format, -1, false, resolution); return card_image; } }; } function TTSDeckPage( busy_props, image_format, image_resolution, page_num, page_cards, copies_list ) { this.rows = Math.min(Math.ceil(Math.sqrt(page_cards.length)), TTS_MAX_ROWS); this.columns = Math.ceil(page_cards.length / this.rows); this.deck_image = null; let deck_graphics; this.card_jsons = []; for (let row = 0; row < this.rows; row++) { for ( let col = 0; col < this.columns && row * this.columns + col < page_cards.length; col++ ) { if (busy_props.cancelled) return; let index = row * this.columns + col; let card = page_cards[index]; busy_props.status = "Processing Card " + card.member; busy_props.currentProgress = (page_num - 1) * TTS_CARDS_PER_IMAGE + index; try { let copies = copyCount(copies_list, card.baseName); for (let ii = 0; ii < copies; ii++) { this.card_jsons.push( TTSJson.makeCardJSON( page_num * 100 + index, card.component.getName() ) ); } let card_image = card.makeImage(image_format, image_resolution); if (!this.deck_image) { this.deck_image = ImageUtils.create( card_image.width * this.columns, card_image.height * this.rows, false ); deck_graphics = this.deck_image.createGraphics(); } deck_graphics.drawImage( card_image, col * card_image.width, row * card_image.height, null ); } catch (ex) { Thread.invokeLater(() => alert("Error while processing " + card + ": " + ex, true) ); } } println("End of Row ", row); } this.face_url = String( getImageFile(page_cards[0].member.parent, image_format, page_num) .toPath() .toUri() ); this.back_url = String( getImageFile(page_cards[0].member.parent, image_format, "back") .toPath() .toUri() ); } function makeTTSDeck( busy_props, image_format, image_resolution, cards, copies_list ) { const pages = []; busy_props.title = "Processing Cards"; busy_props.maximumProgress = cards.length; for ( let page_num = 0; page_num * TTS_CARDS_PER_IMAGE < cards.length; page_num++ ) { let page_cards = cards.slice( page_num * TTS_CARDS_PER_IMAGE, (page_num + 1) * TTS_CARDS_PER_IMAGE ); printf("Making page %d, with %d cards:\n", page_num + 1, page_cards.length); pages.push( new TTSDeckPage( busy_props, image_format, image_resolution, page_num + 1, page_cards, copies_list ) ); if (busy_props.cancelled) return [,]; } const deck_json = TTSJson.makeDeckJSON(pages); return [deck_json, pages.map((page) => page.deck_image)]; } function settingsDialog(deck_task) { const task_settings = deck_task.getSettings(); const image_format_field = comboBox([ ImageUtils.FORMAT_JPEG, ImageUtils.FORMAT_PNG, ]); image_format_field.setSelectedItem( task_settings.get("tts_image_format", "jpg") ); const resolution_field = textField( task_settings.get("tts_image_resolution", "200"), 15 ); const clear_cache_button = button("Clear Cache", undefined, function (e) { const cache_dir = new File(deck_task.file, ".ttsdeck_cache"); cache_dir.listFiles().forEach((file) => file.delete()); }); const panel = new Grid(); // prettier-ignore panel.place( "Image Format", "", image_format_field, "grow,span", "Resolution", "", resolution_field, "grow,span", clear_cache_button, "grow,span" ); const close_button = panel.createDialog("TTS Export").showDialog(); return [ close_button, image_format_field.getSelectedItem(), Number(resolution_field.text), ]; } function run() { const ttsDeckAction = JavaAdapter(TaskAction, { getLabel: () => "Generate TTS Deck", getActionName: () => "ttsdeck", // Applies to Deck Tasks appliesTo: function appliesTo(project, task, member) { if (member != null || task == null) { return false; } const type = task.settings.get(Task.KEY_TYPE); if (NewTaskType.DECK_TYPE.equals(type)) { return true; } return false; }, perform: function perform(project, task, member) { let deck_task = ProjectUtilities.simplify(project, task, member); const [close_button, image_format, image_resolution] = settingsDialog(deck_task); // User canceled the dialog or closed it without pressing ok if (close_button != 1) { return; } // persist settings const task_settings = deck_task.getSettings(); task_settings.set("tts_image_format", image_format); task_settings.set("tts_image_resolution", image_resolution); deck_task.writeTaskSettings(); Eons.setWaitCursor(true); try { Thread.busyWindow( (busy_props) => this.performImpl( busy_props, image_format, image_resolution, deck_task ), "Setting up...", true ); } catch (ex) { Error.handleUncaught(ex); } finally { Eons.setWaitCursor(false); } }, performImpl: function performImpl( busy_props, image_format, image_resolution, member ) { let copies_list; try { copies_list = new CopiesList(member); } catch (ex) { copies_list = new CopiesList(); alert( "unable to read copies list, using card count of 2 for all files", true ); } const children = member.getChildren(); const cards = children .map((child) => { if (ProjectUtilities.matchExtension(child, "eon")) { let card = new Card(child); if (card.component.isDeckLayoutSupported()) { return card; } } return undefined; }) .filter((card) => card !== undefined); const [deck_json, deck_images] = makeTTSDeck( busy_props, image_format, image_resolution, cards, copies_list ); if (busy_props.cancelled) return; const saved_object = TTSJson.makeSavedObjectJSON( [deck_json], member.getName() ); busy_props.status = ""; busy_props.maximumProgress = -1; busy_props.title = "Writing JSON"; const json_file = new File(member.file, member.getName() + ".json"); ProjectUtilities.writeTextFile( json_file, JSON.stringify(saved_object, null, 4) ); busy_props.title = "Writing Images"; busy_props.maximumProgress = deck_images.length; deck_images.forEach((deck_image, index) => { busy_props.currentProgress = index; const image_file = getImageFile(member, image_format, index + 1); ImageUtils.write( deck_image, image_file, image_format, -1, false, image_resolution ); }); let back_image = cards[0].makeImageUncached(image_resolution, true); const back_image_file = getImageFile(member, image_format, "back"); ImageUtils.write( back_image, back_image_file, image_format, -1, false, image_resolution ); member.synchronize(); }, }); ActionRegistry.register(ttsDeckAction, Actions.PRIORITY_IMPORT_EXPORT); }