diff --git a/eons-plugin b/eons-plugin index 5423585..4db9f77 100644 --- a/eons-plugin +++ b/eons-plugin @@ -10,4 +10,4 @@ catalog-homepage = https://github.com/ad1217/SE3-TTSDeck catalog-description = This plugin for allows you to generate images and \ corresponding "Saved Object" JSONs for decks which can be used in Tabletop Simulator. -res://ttsdeck/TTSDeck.js +res://ttsdeck/plugin.js diff --git a/resources/ttsdeck/Card.js b/resources/ttsdeck/Card.js new file mode 100644 index 0000000..d985505 --- /dev/null +++ b/resources/ttsdeck/Card.js @@ -0,0 +1,96 @@ +useLibrary("project"); + +function Card(member, copies_list) { + this.member = member; + this.copies_list = copies_list; + + this.component = ResourceKit.getGameComponentFromFile(member.file); +} + +Card.getImageFile = function getImageFile(parent, format, page_num) { + return new File( + parent.file, + parent.getName() + "_" + page_num + "." + format + ); +}; + +Card.prototype.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 +Card.prototype.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; + } +}; + +// TODO: allow setting a default copy count +// Hack to override the default return value of 1 +Card.prototype.copyCount = function copyCount() { + const entries = this.copies_list.getListEntries().map((x) => String(x)); + if (entries.indexOf(String(this.member.baseName)) == -1) { + return 1; + } else { + return this.copies_list.getCopyCount(this.member.baseName); + } +}; + +Card.prototype.makeJSON = function makeJSON(card_id, description) { + return { + Name: "Card", + Transform: { + posX: 0, + posY: 0, + posZ: 0, + rotX: 0, + rotY: 0, + rotZ: 0, + scaleX: 1.0, + scaleY: 1.0, + scaleZ: 1.0, + }, + Nickname: String(this.component.getName()), + CardID: card_id, + Description: String(description || ""), + ColorDiffuse: { + r: 0.713235259, + g: 0.713235259, + b: 0.713235259, + }, + Locked: false, + Grid: true, + Snap: true, + Autoraise: true, + Sticky: true, + Tooltip: true, + SidewaysCard: false, + }; +}; + +module.exports = Card; diff --git a/resources/ttsdeck/TTSDeck.js b/resources/ttsdeck/TTSDeck.js index 9f7b48f..37118fb 100644 --- a/resources/ttsdeck/TTSDeck.js +++ b/resources/ttsdeck/TTSDeck.js @@ -1,132 +1,61 @@ -/* - * 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 Card = require("./Card.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 TTSDeckPage(image_format, image_resolution, page_num, cards) { + this.image_format = image_format; + this.image_resolution = image_resolution; + this.page_num = page_num; + this.cards = cards; -function unload() { - unregisterAll(); -} + this.rows = Math.min(Math.ceil(Math.sqrt(cards.length)), TTS_MAX_ROWS); + this.columns = Math.ceil(cards.length / this.rows); + this.deck_image = null; + this.card_jsons = []; -// 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 + this.face_url = String( + Card.getImageFile(cards[0].member.parent, image_format, this.page_num) + .toPath() + .toUri() + ); + this.back_url = String( + Card.getImageFile(cards[0].member.parent, image_format, "back") + .toPath() + .toUri() ); } -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; +TTSDeckPage.prototype.build = function build(busy_props) { 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 < this.columns && row * this.columns + col < this.cards.length; col++ ) { - if (busy_props.cancelled) return; + if (busy_props.cancelled) return this; + let index = row * this.columns + col; - let card = page_cards[index]; + let card = this.cards[index]; busy_props.status = "Processing Card " + card.member; - busy_props.currentProgress = (page_num - 1) * TTS_CARDS_PER_IMAGE + index; + busy_props.currentProgress = + (this.page_num - 1) * TTS_CARDS_PER_IMAGE + index; try { - let copies = copyCount(copies_list, card.baseName); + let copies = card.copyCount(); for (let ii = 0; ii < copies; ii++) { - this.card_jsons.push( - TTSJson.makeCardJSON( - page_num * 100 + index, - card.component.getName() - ) - ); + this.card_jsons.push(card.makeJSON(this.page_num * 100 + index)); } - let card_image = card.makeImage(image_format, image_resolution); + let card_image = card.makeImage( + this.image_format, + this.image_resolution + ); if (!this.deck_image) { this.deck_image = ImageUtils.create( @@ -152,225 +81,94 @@ function TTSDeckPage( 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() - ); + return this; +}; + +function TTSDeck(image_format, image_resolution, cards) { + this.image_format = image_format; + this.image_resolution = image_resolution; + this.cards = cards; + + this.pages = []; } -function makeTTSDeck( - busy_props, - image_format, - image_resolution, - cards, - copies_list -) { - const pages = []; - +TTSDeck.prototype.build = function build(busy_props) { busy_props.title = "Processing Cards"; - busy_props.maximumProgress = cards.length; + busy_props.maximumProgress = this.cards.length; for ( let page_num = 0; - page_num * TTS_CARDS_PER_IMAGE < cards.length; + page_num * TTS_CARDS_PER_IMAGE < this.cards.length; page_num++ ) { - let page_cards = cards.slice( + if (busy_props.cancelled) return this; + + let page_cards = this.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( + this.pages.push( new TTSDeckPage( - busy_props, - image_format, - image_resolution, + this.image_format, + this.image_resolution, page_num + 1, - page_cards, - copies_list - ) + page_cards + ).build(busy_props) ); - if (busy_props.cancelled) return [,]; } - const deck_json = TTSJson.makeDeckJSON(pages); + return this; +}; - return [deck_json, pages.map((page) => page.deck_image)]; -} +TTSDeck.prototype.getImages = function getImages() { + return this.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; +TTSDeck.prototype.makeJSON = function makeJSON(nickname, description) { + return { + Name: "DeckCustom", + Transform: { + posX: 0, + posY: 0, + posZ: 0, + rotX: 0, + rotY: 0.0, + rotZ: 0.0, + scaleX: 1.0, + scaleY: 1.0, + scaleZ: 1.0, }, - 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); - } + Nickname: String(nickname || ""), + Description: String(description || ""), + ColorDiffuse: { + r: 0.713239133, + g: 0.713239133, + b: 0.713239133, }, - 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 - ); - } + Grid: true, + Locked: false, + SidewaysCard: false, + DeckIDs: this.pages + .map((page) => page.card_jsons.map((card) => card.CardID)) + .reduce((acc, val) => acc.concat(val), []), + CustomDeck: this.pages.reduce((acc, page, index) => { + acc[String(index + 1)] = { + FaceURL: String(page.face_url), + BackURL: String(page.back_url), + NumWidth: page.columns, + NumHeight: page.rows, + BackIsHidden: true, + }; + return acc; + }, {}), + ContainedObjects: this.pages + .map((page) => page.card_jsons) + .reduce((acc, val) => acc.concat(val), []), + }; +}; - 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); -} +module.exports = { + TTSDeckPage: TTSDeckPage, + TTSDeck: TTSDeck, +}; diff --git a/resources/ttsdeck/TTSJson.js b/resources/ttsdeck/TTSJson.js index c3bbbf7..2784399 100644 --- a/resources/ttsdeck/TTSJson.js +++ b/resources/ttsdeck/TTSJson.js @@ -1,80 +1,5 @@ // Helper functions for Tabletop Simulator JSON output -exports.makeCardJSON = function makeCardJSON(card_id, nickname, description) { - return { - Name: "Card", - Transform: { - posX: 0, - posY: 0, - posZ: 0, - rotX: 0, - rotY: 0, - rotZ: 0, - scaleX: 1.0, - scaleY: 1.0, - scaleZ: 1.0, - }, - Nickname: String(nickname), - CardID: card_id, - Description: String(description || ""), - ColorDiffuse: { - r: 0.713235259, - g: 0.713235259, - b: 0.713235259, - }, - Locked: false, - Grid: true, - Snap: true, - Autoraise: true, - Sticky: true, - Tooltip: true, - SidewaysCard: false, - }; -}; - -exports.makeDeckJSON = function makeDeckJSON(pages, nickname, description) { - return { - Name: "DeckCustom", - Transform: { - posX: 0, - posY: 0, - posZ: 0, - rotX: 0, - rotY: 0.0, - rotZ: 0.0, - scaleX: 1.0, - scaleY: 1.0, - scaleZ: 1.0, - }, - Nickname: String(nickname || ""), - Description: String(description || ""), - ColorDiffuse: { - r: 0.713239133, - g: 0.713239133, - b: 0.713239133, - }, - Grid: true, - Locked: false, - SidewaysCard: false, - DeckIDs: pages - .map((page) => page.card_jsons.map((card) => card.CardID)) - .reduce((acc, val) => acc.concat(val), []), - CustomDeck: pages.reduce((acc, page, index) => { - acc[String(index + 1)] = { - FaceURL: String(page.face_url), - BackURL: String(page.back_url), - NumWidth: page.columns, - NumHeight: page.rows, - BackIsHidden: true, - }; - return acc; - }, {}), - ContainedObjects: pages - .map((page) => page.card_jsons) - .reduce((acc, val) => acc.concat(val), []), - }; -}; - exports.makeSavedObjectJSON = function makeSavedObjectJSON(objects, save_name) { return { SaveName: String(save_name || ""), diff --git a/resources/ttsdeck/plugin.js b/resources/ttsdeck/plugin.js new file mode 100644 index 0000000..6419c48 --- /dev/null +++ b/resources/ttsdeck/plugin.js @@ -0,0 +1,202 @@ +/* + * 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 Card = require("./Card.js"); +const { TTSDeck } = require("./TTSDeck.js"); +const TTSJson = require("./TTSJson.js"); + +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(); + +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, copies_list); + if (card.component.isDeckLayoutSupported()) { + return card; + } + } + return undefined; + }) + .filter((card) => card !== undefined); + + const deck = new TTSDeck( + image_format, + image_resolution, + cards, + copies_list + ).build(busy_props); + + if (busy_props.cancelled) return; + + const saved_object = TTSJson.makeSavedObjectJSON( + [deck.makeJSON()], + 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) + ); + + const deck_images = deck.getImages(); + busy_props.title = "Writing Images"; + busy_props.maximumProgress = deck_images.length; + deck_images.forEach((deck_image, index) => { + busy_props.currentProgress = index; + const image_file = Card.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 = Card.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); +}