diff --git a/plugins/plugin-annotations/src/scalebox.mjs b/plugins/plugin-annotations/src/scalebox.mjs index 3c8278fd197..5a6a2bdaaf1 100644 --- a/plugins/plugin-annotations/src/scalebox.mjs +++ b/plugins/plugin-annotations/src/scalebox.mjs @@ -1,34 +1,25 @@ -// Export macros -export const scaleboxMacros = { - scalebox: function (so, { store, points, paths, scale, Point, Path }) { - // Passing `false` will remove the scalebox - if (so === false) { - for (let id of [ - '__scaleboxMetricTopLeft', - '__scaleboxMetricTopRight', - '__scaleboxMetricBottomRight', - '__scaleboxMetricBottomLeft', - '__scaleboxImperialTopLeft', - '__scaleboxImperialTopRight', - '__scaleboxImperialBottomRight', - '__scaleboxImperialBottomLeft', - '__scaleboxLead', - '__scaleboxTitle', - '__scaleboxText', - '__scaleboxLink', - '__scaleboxMetric', - '__scaleboxImperial', - ]) - delete points[id] - for (let id of ['__scaleboxMetric', '__scaleboxImperial']) delete paths[id] - return true - } +import { getIds } from './utils.mjs' - // Convert scale to a value between 0 and 9, inclusive. - const scaleIndex = Math.round(10 * Math.max(0.1, Math.min(1, scale))) - 1 +/* + * Defaults for the title macro + */ +const macroDefaults = { + classes: { + lead: 'text-xs bold', + title: 'text bold', + text: 'text-sm', + link: 'text-sm fill-note', + metric: 'text-xs center', + imperial: 'text-xs center', + }, +} - // Metric width and height in mm and display width and height for each scale index. - const metricSizes = [ +/* + * Various sizes for scaleboxes per units + */ +const sizes = { + scalebox: { + metric: [ [10, 5, '1cm', '0.5cm'], [20, 10, '2cm', '1cm'], [30, 15, '3cm', '1.5cm'], @@ -39,15 +30,8 @@ export const scaleboxMacros = { [80, 40, '8cm', '4cm'], [90, 45, '9cm', '4.5cm'], [100, 50, '10cm', '5cm'], - ] - - const metricWidth = metricSizes[scaleIndex][0] - const metricHeight = metricSizes[scaleIndex][1] - const metricDisplayWidth = metricSizes[scaleIndex][2] - const metricDisplayHeight = metricSizes[scaleIndex][3] - - // Imperial width and height in mm and display width and heigth for each scale index. - const imperialSizes = [ + ], + imperial: [ [25.4 * 0.5, 25.4 * 0.25, '½″', '¼″'], [25.4 * 0.875, 25.4 * 0.5, '⅞″', '½″'], [25.4 * 1.25, 25.4 * 0.625, '1 ¼″', '⅝″'], @@ -58,225 +42,358 @@ export const scaleboxMacros = { [25.4 * 3.25, 25.4 * 1.625, '3 ¼″', '1 ⅝″'], [25.4 * 3.625, 25.4 * 1.875, '3 ⅝″', '1 ⅞″'], [25.4 * 4, 25.4 * 2, '4″', '2″'], - ] - - const imperialWidth = imperialSizes[scaleIndex][0] - const imperialHeight = imperialSizes[scaleIndex][1] - const imperialDisplayWidth = imperialSizes[scaleIndex][2] - const imperialDisplayHeight = imperialSizes[scaleIndex][3] - - // Box points - points.__scaleboxMetricTopLeft = new Point( - so.at.x - metricWidth / 2, - so.at.y - metricHeight / 2 - ) - points.__scaleboxMetricTopRight = new Point( - so.at.x + metricWidth / 2, - so.at.y - metricHeight / 2 - ) - points.__scaleboxMetricBottomLeft = new Point( - so.at.x - metricWidth / 2, - so.at.y + metricHeight / 2 - ) - points.__scaleboxMetricBottomRight = new Point( - so.at.x + metricWidth / 2, - so.at.y + metricHeight / 2 - ) - points.__scaleboxImperialTopLeft = new Point( - so.at.x - imperialWidth / 2, - so.at.y - imperialHeight / 2 - ) - points.__scaleboxImperialTopRight = new Point( - so.at.x + imperialWidth / 2, - so.at.y - imperialHeight / 2 - ) - points.__scaleboxImperialBottomLeft = new Point( - so.at.x - imperialWidth / 2, - so.at.y + imperialHeight / 2 - ) - points.__scaleboxImperialBottomRight = new Point( - so.at.x + imperialWidth / 2, - so.at.y + imperialHeight / 2 - ) - // Text anchor points - points.__scaleboxLead = new Point(so.at.x - 45 * scale, so.at.y - 15 * scale) - points.__scaleboxTitle = points.__scaleboxLead.shift(-90, 10 * scale) - points.__scaleboxText = points.__scaleboxTitle.shift(-90, 12 * scale) - points.__scaleboxLink = points.__scaleboxText.shift(-90, 5 * scale) - points.__scaleboxMetric = new Point(so.at.x, so.at.y + 20 * scale) - points.__scaleboxImperial = new Point(so.at.x, so.at.y + 24 * scale) - // Rotation - if (so.rotate) { - so.rotate = Number(so.rotate) - let toRotate = [ - '__scaleboxMetricTopLeft', - '__scaleboxMetricTopRight', - '__scaleboxMetricBottomLeft', - '__scaleboxMetricBottomRight', - '__scaleboxImperialTopLeft', - '__scaleboxImperialTopRight', - '__scaleboxImperialBottomLeft', - '__scaleboxImperialBottomRight', - '__scaleboxLead', - '__scaleboxTitle', - '__scaleboxText', - '__scaleboxLink', - '__scaleboxMetric', - '__scaleboxImperial', - ] - for (let pid of toRotate) points[pid] = points[pid].rotate(so.rotate, so.at) - for (let pid of toRotate.slice(8)) { - points[pid].attributes.set( - 'data-text-transform', - `rotate(${so.rotate * -1}, ${points[pid].x}, ${points[pid].y})` - ) - } - } - // Paths - paths.__scaleboxImperial = new Path() - .attr('class', 'scalebox imperial fill-current') - .move(points.__scaleboxImperialTopLeft) - .line(points.__scaleboxImperialBottomLeft) - .line(points.__scaleboxImperialBottomRight) - .line(points.__scaleboxImperialTopRight) - .close() - paths.__scaleboxMetric = new Path() - .attr('class', 'scalebox metric fill-bg') - .move(points.__scaleboxMetricTopLeft) - .line(points.__scaleboxMetricBottomLeft) - .line(points.__scaleboxMetricBottomRight) - .line(points.__scaleboxMetricTopRight) - .close() - // Lead - points.__scaleboxLead = points.__scaleboxLead - .attr('data-text', so.lead || 'FreeSewing') - .attr('data-text-class', 'text-sm') - // Title - if (so.title) points.__scaleboxTitle.attributes.set('data-text', so.title) - else { - let name = store.data?.name || 'No Name' - if (name.indexOf('@freesewing/') !== -1) name = name.replace('@freesewing/', '') - points.__scaleboxTitle = points.__scaleboxTitle - .attr('data-text', name) - .attr('data-text', 'v' + (store.data?.version || 'No Version')) - } - points.__scaleboxTitle.attributes.add('data-text-class', 'text-lg') - // Text - if (typeof so.text === 'string') { - points.__scaleboxText.attr('data-text', so.text) - } else { - points.__scaleboxText.attr('data-text', 'supportFreesewingBecomeAPatron') - points.__scaleboxLink = points.__scaleboxLink - .attr('data-text', 'freesewing.org/patrons/join') - .attr('data-text-class', 'text-sm fill-note') - } - points.__scaleboxText.attr('data-text-class', 'text-xs').attr('data-text-lineheight', 4) - // Instructions - points.__scaleboxMetric = points.__scaleboxMetric - .attr('data-text', 'theWhiteInsideOfThisBoxShouldMeasure') - .attr('data-text', `${metricDisplayWidth}`) - .attr('data-text', 'x') - .attr('data-text', `${metricDisplayHeight}`) - .attr('data-text-class', 'text-xs center') - points.__scaleboxImperial = points.__scaleboxImperial - .attr('data-text', 'theBlackOutsideOfThisBoxShouldMeasure') - .attr('data-text', `${imperialDisplayWidth}`) - .attr('data-text', 'x') - .attr('data-text', `${imperialDisplayHeight}`) - .attr('data-text-class', 'text-xs center ') - }, - miniscale(so, { points, paths, scale, Point, Path }) { - // Passing `false` will remove the miniscale - if (so === false) { - for (const id of [ - '__miniscaleMetricTopLeft', - '__miniscaleMetricTopRight', - '__miniscaleMetricBottomRight', - '__miniscaleMetricBottomLeft', - '__miniscaleImperialTopLeft', - '__miniscaleImperialTopRight', - '__miniscaleImperialBottomRight', - '__miniscaleImperialBottomLeft', - '__miniscaleMetric', - '__miniscaleImperial', - ]) - delete points[id] - for (const id of ['__miniscaleMetric', '__miniscaleImperial']) delete paths[id] - return true - } - - // Convert scale to a value between 0 and 5, inclusive. - const scaleIndex = Math.ceil(6 * Math.max(0.1, Math.min(1, scale))) - 1 - - // Metric size in mm / display value and imperial size in mm / display value for each scale index. - const sizes = [ - [10, '1cm', 25.4 * 0.375, '⅜″'], - [13, '1.3cm', 25.4 * 0.5, '½″'], - [16, '1.6cm', 25.4 * 0.625, '⅝″'], - [19, '1.9cm', 25.4 * 0.75, '¾″'], - [22, '2.2cm', 25.4 * 0.875, '⅞″'], - [25, '2.5cm', 25.4 * 1, '1″'], - ] - const m = sizes[scaleIndex][0] / 2 - const i = sizes[scaleIndex][2] / 2 - const metricDisplaySize = sizes[scaleIndex][1] - const imperialDisplaySize = sizes[scaleIndex][3] - // Box points - points.__miniscaleMetricTopLeft = new Point(so.at.x - m, so.at.y - m) - points.__miniscaleMetricTopRight = new Point(so.at.x + m, so.at.y - m) - points.__miniscaleMetricBottomLeft = new Point(so.at.x - m, so.at.y + m) - points.__miniscaleMetricBottomRight = new Point(so.at.x + m, so.at.y + m) - points.__miniscaleImperialTopLeft = new Point(so.at.x - i, so.at.y - i) - points.__miniscaleImperialTopRight = new Point(so.at.x + i, so.at.y - i) - points.__miniscaleImperialBottomLeft = new Point(so.at.x - i, so.at.y + i) - points.__miniscaleImperialBottomRight = new Point(so.at.x + i, so.at.y + i) - // Text anchor points - points.__miniscaleMetric = new Point(so.at.x, so.at.y - 2 * scale) - points.__miniscaleImperial = new Point(so.at.x, so.at.y + 8 * scale) - // Rotation - if (so.rotate) { - so.rotate = Number(so.rotate) - let toRotate = [ - '__miniscaleMetricTopLeft', - '__miniscaleMetricTopRight', - '__miniscaleMetricBottomLeft', - '__miniscaleMetricBottomRight', - '__miniscaleImperialTopLeft', - '__miniscaleImperialTopRight', - '__miniscaleImperialBottomLeft', - '__miniscaleImperialBottomRight', - '__miniscaleMetric', - '__miniscaleImperial', - ] - for (const pid of toRotate) points[pid] = points[pid].rotate(so.rotate, so.at) - for (const pid of toRotate.slice(8)) { - points[pid].attributes.set( - 'data-text-transform', - `rotate(${so.rotate * -1}, ${points[pid].x}, ${points[pid].y})` - ) - } - } - // Paths - paths.__miniscaleImperial = new Path() - .attr('class', 'scalebox imperial fill-current') - .move(points.__miniscaleImperialTopLeft) - .line(points.__miniscaleImperialBottomLeft) - .line(points.__miniscaleImperialBottomRight) - .line(points.__miniscaleImperialTopRight) - .close() - paths.__miniscaleMetric = new Path() - .attr('class', 'scalebox metric fill-bg') - .move(points.__miniscaleMetricTopLeft) - .line(points.__miniscaleMetricBottomLeft) - .line(points.__miniscaleMetricBottomRight) - .line(points.__miniscaleMetricTopRight) - .close() - // Text - points.__miniscaleMetric = points.__miniscaleMetric - .attr('data-text', `${metricDisplaySize} x ${metricDisplaySize}`) - .attr('data-text-class', 'text-xs center') - points.__miniscaleImperial = points.__miniscaleImperial - .attr('data-text', `${imperialDisplaySize} x ${imperialDisplaySize}`) - .attr('data-text-class', 'text-xs center ') + ], }, + miniscale: [ + [10, '1cm', 25.4 * 0.375, '⅜″'], + [13, '1.3cm', 25.4 * 0.5, '½″'], + [16, '1.6cm', 25.4 * 0.625, '⅝″'], + [19, '1.9cm', 25.4 * 0.75, '¾″'], + [22, '2.2cm', 25.4 * 0.875, '⅞″'], + [25, '2.5cm', 25.4 * 1, '1″'], + ], +} + +/* + * This removes a given macro type + */ +const removeScaleAnnotation = function (id = false, { paths, points, store, part }, type) { + if (!id) id = type + const both = store.get(['parts', part.name, 'macros', type, 'ids', id], { paths: {}, points: {} }) + for (const pid of Object.values(both.points)) delete points[pid] + for (const pid of Object.values(both.paths)) delete paths[pid] +} + +/* + * The scalebox macro + */ +const scalebox = function (config, { store, points, paths, scale, Point, Path, complete, part }) { + /* + * Don't add a title when complete is false, unless force is true + */ + if (!complete && !config.force) return + + /* + * Merge macro defaults with user-provided config to create the macro config (mc) + */ + const mc = { + ...macroDefaults, + id: 'scalebox', + ...config, + classes: macroDefaults.classes, + } + if (config.classes) mc.classes = { ...mc.classes, ...config.classes } + + /* + * Figure out what size to use + * We convert scale to a value between 0 and 9, inclusive. + * Then pick the right size from the sizes[units] array. + * Array holds width, height, displayWidth, displayHeight + */ + const scaleIndex = Math.round(10 * Math.max(0.1, Math.min(1, scale))) - 1 + const [mw, mh, mdw, mdh] = sizes.scalebox.metric[scaleIndex] + const [iw, ih, idw, idh] = sizes.scalebox.imperial[scaleIndex] + + /* + * Make sure mc.at is a Point instance + */ + if (!mc.at || typeof mc.at.attr !== 'function') { + log.warn(`Scalebox macro called without a valid at point. Using (0,0) for at.`) + mc.from = new Point(0, 0) + } + + /* + * Get the list of IDs + */ + const ids = getIds( + [ + 'metric', + 'imperial', + 'textLead', + 'textMetric', + 'textImperial', + 'textTitle', + 'textText', + 'textLink', + ], + mc.id, + 'scalebox' + ) + + /* + * Box points (no need to add these to the part) + */ + const box = { + mtl: new Point(mc.at.x - mw / 2, mc.at.y - mh / 2), + mtr: new Point(mc.at.x + mw / 2, mc.at.y - mh / 2), + mbl: new Point(mc.at.x - mw / 2, mc.at.y + mh / 2), + mbr: new Point(mc.at.x + mw / 2, mc.at.y + mh / 2), + itl: new Point(mc.at.x - iw / 2, mc.at.y - ih / 2), + itr: new Point(mc.at.x + iw / 2, mc.at.y - ih / 2), + ibl: new Point(mc.at.x - iw / 2, mc.at.y + ih / 2), + ibr: new Point(mc.at.x + iw / 2, mc.at.y + ih / 2), + } + + /* + * Text points + */ + const text = { + lead: new Point(mc.at.x - 45 * scale, mc.at.y - 15 * scale), + metric: new Point(mc.at.x, mc.at.y + 20 * scale), + imperial: new Point(mc.at.x, mc.at.y + 24 * scale), + } + text.title = text.lead.shift(-90, 10 * scale) + text.text = text.title.shift(-90, 12 * scale) + text.link = text.text.shift(-90, 5 * scale) + + /* + * Handle rotation if needed + */ + if (mc.rotate) { + mc.rotate = Number(mc.rotate) + for (const pid in box) box[pid] = box[pid].rotate(mc.rotate, mc.at) + for (const pid in text) { + text[pid] = text[pid] + .rotate(mc.rotate, mc.at) + .attr( + 'data-text-transform', + `rotate(${mc.rotate * -1}, ${text[pid].x}, ${text[pid].y})`, + true + ) + } + } + + /* + * Draw the imperial box + */ + paths[ids.imperial] = new Path() + .attr('class', 'scalebox imperial fill-current') + .move(box.itl) + .line(box.ibl) + .line(box.ibr) + .line(box.itr) + .line(box.itl) + .close() + + /* + * Draw the metric box + */ + paths[ids.metric] = new Path() + .attr('class', 'scalebox metric fill-bg') + .move(box.mtl) + .line(box.mbl) + .line(box.mbr) + .line(box.mtr) + .line(box.mtl) + .close() + + /* + * Add lead text to the part points + */ + points[ids.textLead] = text.lead + .addText(mc.lead || 'FreeSewing', mc.classes.lead) + .attr('data-text-class', 'text-sm') + + /* + * Figure out what title to use, and add the title text to the part points + */ + let title = mc.title + if (!title) { + title = store.data?.name || 'No Name' + if (title.indexOf('@freesewing/') !== -1) title = title.replace('@freesewing/', '') + } + points[ids.textTitle] = text.title + .addText(title, mc.classes.title) + .attr('data-text', 'v' + (store.data?.version || 'No Version')) + + /* + * Add text text to the part points + */ + points[ids.textText] = text.text.addText( + typeof mc.text === 'string' ? mc.text : 'supportFreesewingBecomeAPatron', + mc.classes.text + ) + + /* + * Add link text to the part points + */ + points[ids.textLink] = text.link + .addText('freesewing.org/patrons/join', mc.classes.link) + .attr('data-text-lineheight', 4) + + /* + * Add metric instructions text to the part points + */ + points[ids.textMetric] = text.metric + .attr('data-text', 'theWhiteInsideOfThisBoxShouldMeasure') + .attr('data-text', mdw) + .attr('data-text', 'x') + .attr('data-text', mdh) + .attr('data-text-class', mc.classes.metric) + + /* + * Add imperial instructions text to the part points + */ + points[ids.textImperial] = text.imperial + .attr('data-text', 'theBlackOutsideOfThisBoxShouldMeasure') + .attr('data-text', idw) + .attr('data-text', 'x') + .attr('data-text', idh) + .attr('data-text-class', mc.classes.imperial) + + /* + * Store all IDs in the store so we can remove this macro with rmscaleboc + */ + store.set(['parts', part.name, 'macros', 'scalebox', 'ids', mc.id], { + points: { + textLead: ids.textLead, + textMetric: ids.textMetric, + textImperial: ids.textImperial, + textTitle: ids.textTitle, + textText: ids.textText, + textLink: ids.textLink, + }, + paths: { + metric: ids.metric, + imperial: ids.imperial, + }, + }) +} + +/* + * The miniscale macro + */ +const miniscale = function (config, { points, paths, scale, Point, Path, part, complete, store }) { + /* + * Don't add a title when complete is false, unless force is true + */ + if (!complete && !config.force) return + + /* + * Merge macro defaults with user-provided config to create the macro config (mc) + */ + const mc = { + ...macroDefaults, + id: 'miniscale', + ...config, + classes: macroDefaults.classes, + } + if (config.classes) mc.classes = { ...mc.classes, ...config.classes } + + /* + * Figure out what size to use + * We convert scale to a value between 0 and 5, inclusive. + * Then pick the right size from the sizes.miniscale array. + * Array holds metricSize, metricDisplaySize, imperialSize, imperialDisplaySize + */ + const scaleIndex = Math.ceil(6 * Math.max(0.1, Math.min(1, scale))) - 1 + const [ms, mds, is, imds] = sizes.miniscale[scaleIndex] + + /* + * Make sure mc.at is a Point instance + */ + if (!mc.at || typeof mc.at.attr !== 'function') { + log.warn(`Scalebox macro called without a valid at point. Using (0,0) for at.`) + mc.from = new Point(0, 0) + } + + /* + * Get the list of IDs + */ + const ids = getIds(['metric', 'imperial', 'textMetric', 'textImperial'], mc.id, 'miniscale') + + /* + * Box points (no need to add these to the part) + */ + const box = { + mtl: new Point(mc.at.x - ms / 2, mc.at.y - ms / 2), + mtr: new Point(mc.at.x + ms / 2, mc.at.y - ms / 2), + mbl: new Point(mc.at.x - ms / 2, mc.at.y + ms / 2), + mbr: new Point(mc.at.x + ms / 2, mc.at.y + ms / 2), + itl: new Point(mc.at.x - is / 2, mc.at.y - is / 2), + itr: new Point(mc.at.x + is / 2, mc.at.y - is / 2), + ibl: new Point(mc.at.x - is / 2, mc.at.y + is / 2), + ibr: new Point(mc.at.x + is / 2, mc.at.y + is / 2), + } + + /* + * Text points + */ + const text = { + metric: new Point(mc.at.x, mc.at.y - 2 * scale), + imperial: new Point(mc.at.x, mc.at.y + 8 * scale), + } + + /* + * Handle rotation if needed + */ + if (mc.rotate) { + mc.rotate = Number(mc.rotate) + for (const pid in box) box[pid] = box[pid].rotate(mc.rotate, mc.at) + for (const pid in text) { + text[pid] = text[pid] + .rotate(mc.rotate, mc.at) + .attr( + 'data-text-transform', + `rotate(${mc.rotate * -1}, ${text[pid].x}, ${text[pid].y})`, + true + ) + } + } + + /* + * Draw the imperial box + */ + paths[ids.imperial] = new Path() + .attr('class', 'scalebox imperial fill-current') + .move(box.itl) + .line(box.ibl) + .line(box.ibr) + .line(box.itr) + .line(box.itl) + .close() + + /* + * Draw the metric box + */ + paths[ids.metric] = new Path() + .attr('class', 'scalebox metric fill-bg') + .move(box.mtl) + .line(box.mbl) + .line(box.mbr) + .line(box.mtr) + .line(box.mtl) + .close() + + /* + * Add metric text to the part points + */ + points[ids.textMetric] = text.metric.addText(`${mds} x ${mds}`, mc.classes.metric) + + /* + * Add imperial text to the part points + */ + points[ids.textImperial] = text.imperial.addText(`${imds} x ${imds}`, mc.classes.imperial) + + /* + * Store all IDs in the store so we can remove this macro with rmscaleboc + */ + store.set(['parts', part.name, 'macros', 'miniscale', 'ids', mc.id], { + points: { + textMetric: ids.textMetric, + textImperial: ids.textImperial, + }, + paths: { + metric: ids.metric, + imperial: ids.imperial, + }, + }) +} + +// Export macros +export const scaleboxMacros = { + scalebox, + miniscale, + rmscalebox: (id, props) => removeScaleAnnotation(id, props, 'scalebox'), + rmminiscale: (id, props) => removeScaleAnnotation(id, props, 'miniscale'), }