1
0
Fork 0

Merge branch 'joost' into plugins-scale

This commit is contained in:
Joost De Cock 2023-10-15 16:05:28 +02:00
commit d739e8f5bd
24466 changed files with 405611 additions and 707715 deletions

View file

@ -1,23 +1,66 @@
// Export macros
export const bannerMacros = {
banner: function (so) {
// Mix defaults with settings object
so = {
text: '',
dy: -1,
spaces: 12,
repeat: 10,
className: '',
...so,
}
so.path.attr('data-text-dy', so.dy).attr('data-text-class', `${so.className} center`)
const spacer = ' '.repeat(so.spaces)
import { getIds } from './utils.mjs'
for (let i = 0; i < so.repeat; i++) {
so.path.attr('data-text', spacer)
so.path.attr('data-text', so.text)
}
so.path.attr('data-text', spacer)
},
/*
* Defaults for the bannner macro
*/
const macroDefaults = {
classes: 'center',
dy: -1,
force: false,
id: 'banner',
repeat: 10,
spaces: 12,
}
/*
* The rmbanner macro
*/
const rmbanner = function (id = macroDefaults.id, { paths, store, part }) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', 'banner', 'ids', id, 'paths'], {})
))
delete paths[pid]
}
const banner = function (config, { part, paths, store, complete }) {
/*
* Don't add a banne 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, ...config }
/*
* Get the list of IDs
*/
const ids = getIds(['banner'], mc.id, 'banner')
/*
* Prepare the path to hold the banner text
*/
paths[ids.banner] = mc.path
.clone()
.setClass('hidden')
.attr('data-text-dy', mc.dy)
.attr('data-text-class', mc.classes)
/*
* Construct the text string piece by piece so it gets translated
*/
const spacer = '&#160;'.repeat(mc.spaces)
for (let i = 0; i < mc.repeat; i++) paths[ids.banner].addText(mc.text).addText(spacer)
paths[ids.banner].addText(mc.text)
/*
* Store all IDs in the store so we can remove this macro with rmbanner
*/
store.set(['parts', part.name, 'macros', 'banner', 'ids', mc.id, 'paths'], ids)
return store.getMacroIds(config.id, 'banner')
}
// Export macros
export const bannerMacros = { banner, rmbanner }

View file

@ -1,37 +1,107 @@
// Export macros
export const bannerboxMacros = {
bannerbox: function (so, { Point, paths, Path, getId, macro }) {
// Spread so settings into defaults
so = {
topLeft: new Point(0, 0),
bottomRight: new Point(100, 100),
text: '',
margin: 15,
textClassName: 'text-xs fill-note',
boxClassName: 'stroke-xs stroke-note lashed',
dy: 4,
spaces: 12,
repeat: 10,
...so,
}
const offset = Math.sqrt(2 * Math.pow(so.margin, 2))
import { getIds } from './utils.mjs'
const id = getId()
paths[id] = new Path()
.move(so.topLeft.shift(135, offset))
.line(new Point(so.bottomRight.x, so.topLeft.y).shift(45, offset))
.line(so.bottomRight.shift(315, offset))
.line(new Point(so.topLeft.x, so.bottomRight.y).shift(225, offset))
.line(so.topLeft.shift(135, offset))
.close()
.addClass(so.boxClassName)
macro('banner', {
path: paths[id],
text: so.text,
className: so.textClassName,
repeat: 99,
dy: 4,
})
/*
* Defaults for the bannerbox macro
*/
const macroDefaults = {
classes: {
text: 'text-xs fill-note',
box: 'stroke-xs stroke-note lashed',
},
dy: 4,
id: 'bannerbox',
margin: 15,
repeat: 99,
spaces: 12,
text: '',
}
/*
* Removing all this is easy as all IDs are available in the store
* and all we need to remove are paths.
*/
const rmbannerbox = function (id = macroDefaults.id, { paths, store, part, macro }) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', 'bannerbox', 'ids', id, 'paths'], {})
))
delete paths[pid]
macro('rmbanner', id)
}
/*
* The bannerbox macro
*/
const bannerbox = function (config, { Point, paths, Path, part, macro, log, store, complete }) {
/*
* 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,
...config,
classes: macroDefaults.classes,
}
if (config.classes) mc.classes = { ...mc.classes, ...config.classes }
/*
* Make sure mc.topLeft and mc.bottomRight are Point instances
*/
if (!mc.topLeft || typeof mc.topLeft.attr !== 'function') {
log.warn(`Bannerbox macro called without a valid topLeft point. Using (0,0) for topLeft.`)
mc.topLeft = new Point(0, 0)
}
if (!mc.bottomRight || typeof mc.bottomRight.attr !== 'function') {
log.warn(
`Bannerbox macro called without a valid bottomRight point. Using (6660,666) for bottomRight.`
)
mc.bottomRight = new Point(666, 666)
}
/*
* Get the list of IDs
*/
const ids = getIds(['box'], mc.id, 'bannerbox')
/*
* Calculate the offset from the bounding box
*/
const offset = Math.sqrt(2 * Math.pow(mc.margin, 2))
/*
* Bannerbox: box
*/
paths[ids.box] = new Path()
.move(mc.topLeft.shift(135, offset))
.line(new Point(mc.bottomRight.x, mc.topLeft.y).shift(45, offset))
.line(mc.bottomRight.shift(315, offset))
.line(new Point(mc.topLeft.x, mc.bottomRight.y).shift(225, offset))
.line(mc.topLeft.shift(135, offset))
.close()
.addClass(mc.classes.box)
/*
* Call the banner macro on the box
*/
macro('banner', {
id: mc.id,
path: paths[ids.box],
text: mc.text,
className: mc.classes.text,
repeat: mc.repeat,
spaces: mc.spaces,
dy: mc.dy,
})
/*
* Store all IDs in the store so we can remove this macro with rmtitle
*/
store.set(['parts', part.name, 'macros', 'bannerbox', 'ids', mc.id, 'paths'], ids)
return store.getMacroIds(mc.id, 'bannerbox')
}
export const bannerboxMacros = { bannerbox, rmbannerbox }

View file

@ -1,3 +1,25 @@
import { getIds } from './utils.mjs'
/*
* Defaults for the bartack macro
*/
const macroDefaults = {
anchor: false,
angle: 0,
bartackAlong: false,
bartackFractionAlong: false,
classes: 'stroke-sm stroke-mark',
density: 3,
end: 1,
from: false,
id: 'bartack',
length: 15,
path: false,
start: 0,
to: false,
width: 3,
}
// Method that draws the actual bartack
const drawBartack = (pointList, { Path }) => {
let path = new Path().move(pointList.path1[0])
@ -10,9 +32,9 @@ const drawBartack = (pointList, { Path }) => {
}
// Helper method to generate the points to draw on later
const getPoints = (path, so) => {
let path1 = path.offset(so.width / 2)
let path2 = path.offset(so.width / -2)
const getPoints = (path, mc) => {
let path1 = path.offset(mc.width / 2)
let path2 = path.offset(mc.width / -2)
let len = path1.length()
let len2 = path2.length()
@ -28,7 +50,7 @@ const getPoints = (path, so) => {
path1: [path1.start()],
path2: [path2.start()],
}
let steps = Math.ceil((len / so.width) * so.density)
let steps = Math.ceil((len / mc.width) * mc.density)
for (let i = 1; i < steps; i++) {
points.path1.push(path1.shiftFractionAlong((1 / steps) * i))
points.path2.push(path2.shiftFractionAlong((1 / steps) * i))
@ -37,90 +59,140 @@ const getPoints = (path, so) => {
return points
}
const bartackPath = (path, so, props) => (path ? drawBartack(getPoints(path, so), props) : null)
const bartackPath = (path, mc, props) => (path ? drawBartack(getPoints(path, mc), props) : null)
function createBartack(so, props) {
const defaults = {
width: 3,
length: 15,
density: 3,
angle: 0,
prefix: '',
suffix: '',
anchor: false,
path: false,
from: false,
to: false,
start: 0,
end: 1,
bartackAlong: false,
bartackFractionAlong: false,
}
so = { ...defaults, ...so }
/*
* This method creates the actual bartack
*/
function createBartack(config, props) {
/*
* Don't add a bartack when complete is false, unless force is true
*/
if (!props.complete && !config.force) return
const { Path, paths, scale } = props
/*
* Merge macro defaults with user-provided config to create the macro config (mc)
*/
const mc = { ...macroDefaults, ...config }
so.width *= scale
/*
* Destructure what we need from props
*/
const { Path, paths } = props
// Handle negative angle
if (so.angle < 0) so.angle = 360 + (so.angle % -360)
/*
* Handle negative angle
*/
if (mc.angle < 0) mc.angle = 360 + (mc.angle % -360)
/*
* Construct the guide path
*/
let guide = false
if (so.anchor)
let name = 'bartack'
if (mc.anchor)
// Anchor + angle + length
guide = new Path().move(so.anchor).line(so.anchor.shift(so.angle, so.length))
else if (so.from && so.to)
guide = new Path().move(mc.anchor).line(mc.anchor.shift(mc.angle, mc.length))
else if (mc.from && mc.to)
// From to
guide = new Path().move(so.from).line(so.to)
else if (so.path) {
guide = new Path().move(mc.from).line(mc.to)
else if (mc.path) {
// Along path
let start = false
let end = false
if (so.bartackAlong) guide = so.path.clone()
else if (so.bartackFractionAlong) {
if (so.start === so.end) return null
if (so.start > so.end) {
const newEnd = so.start
so.start = so.end
so.end = newEnd
name = 'bartackalong'
if (mc.bartackAlong) guide = mc.path.clone()
else if (mc.bartackFractionAlong) {
name = 'bartackfractionalong'
if (mc.start === mc.end) return null
if (mc.start > mc.end) {
const newEnd = mc.start
mc.start = mc.end
mc.end = newEnd
}
if (so.start > 0) start = so.path.shiftFractionAlong(so.start)
if (so.end < 1) end = so.path.shiftFractionAlong(so.end)
if (start && end) guide = so.path.split(start).pop().split(end).shift()
else if (start) guide = so.path.split(start).pop()
else if (end) guide = so.path.split(end).shift()
else guide = so.path.clone()
if (mc.start > 0) start = mc.path.shiftFractionAlong(mc.start)
if (mc.end < 1) end = mc.path.shiftFractionAlong(mc.end)
if (start && end) guide = mc.path.split(start).pop().split(end).shift()
else if (start) guide = mc.path.split(start).pop()
else if (end) guide = mc.path.split(end).shift()
else guide = mc.path.clone()
}
}
paths[`${so.prefix}bartack${so.suffix}`] = bartackPath(guide, so, props).attr(
'class',
'stroke-sm stroke-mark'
/*
* Get the list of IDs
*/
const ids = getIds(['stitches'], mc.id, name)
paths[ids.stitches] = bartackPath(guide, mc, props).attr('class', mc.classes)
/*
* Store all IDs in the store so we can remove this macro with rm[name]
*/
props.store.set(['parts', props.part.name, 'macros', name, 'ids', mc.id, 'paths'], ids)
return props.store.getMacroIds(mc.id, name)
}
/*
* The method that will remove all macros
*/
const removeBartack = function (name = 'bartack', id = macroDefaults.id, { paths, store, part }) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', name, 'ids', id, 'paths'], {})
))
delete paths[pid]
}
/*
* The rmbartackalong and rmbartackfractionalong macros just call rmbartack with the correct name
*/
const rmbartack = (id, props) => removeBartack('bartack', id, props)
const rmbartackAlong = (id, props) => removeBartack('bartackalong', id, props)
const rmbartackFractionAlong = (id, props) => removeBartack('bartackfractionalong', id, props)
/*
* The bartack macro
*/
const bartack = (config, props) => createBartack(config, props)
/*
* The bartackAlong macro
*/
const bartackAlong = (config, props) =>
createBartack(
{
...config,
bartackFractionAlong: false,
bartackAlong: true,
anchor: false,
from: false,
to: false,
},
props
)
return true
}
/*
* The bartackFractionAlong macro
*/
const bartackFractionAlong = (config, props) =>
createBartack(
{
...config,
bartackFractionAlong: true,
bartackAlong: false,
anchor: false,
from: false,
to: false,
},
props
)
// Export macros
export const bartackMacros = {
bartack: function (so, props) {
return createBartack(so, props)
},
bartackAlong: function (so, props) {
so.bartackFractionAlong = false
so.bartackAlong = true
so.anchor = false
so.from = false
so.to = false
return createBartack(so, props)
},
bartackFractionAlong: function (so, props) {
so.bartackFractionAlong = true
so.bartackAlong = false
so.anchor = false
so.from = false
so.to = false
return createBartack(so, props)
},
bartack,
bartackAlong,
bartackFractionAlong,
rmbartack,
rmbartackAlong,
rmbartackFractionAlong,
}

View file

@ -8,11 +8,11 @@ transform="scale(${scale})">
<circle
cx="0" cy="0" r="3.4"
class="mark"
/>
<circle cx="-1" cy="-1" r="0.5" class="no-stroke fill-mark"/>
<circle cx="1" cy="-1" r="0.5" class="no-stroke fill-mark" />
<circle cx="1" cy="1" r="0.5" class="no-stroke fill-mark" />
<circle cx="-1" cy="1" r="0.5" class="no-stroke fill-mark" />
></circle>
<circle cx="-1" cy="-1" r="0.5" class="no-stroke fill-mark"></circle>
<circle cx="1" cy="-1" r="0.5" class="no-stroke fill-mark"></circle>
<circle cx="1" cy="1" r="0.5" class="no-stroke fill-mark"></circle>
<circle cx="-1" cy="1" r="0.5" class="no-stroke fill-mark"></circle>
</g>`,
},
{
@ -23,7 +23,7 @@ transform="scale(${scale})">
<path
class="mark"
d="M -1,-5 L 1,-5 L 1,5 L -1,5 z"
/>
></path>
</g>`,
},
{
@ -34,7 +34,7 @@ transform="scale(${scale})">
<path
class="mark"
d="M -1,-10 L 1,-10 L 1,0 L -1,0 z"
/>
></path>
</g>`,
},
{
@ -45,7 +45,7 @@ transform="scale(${scale})">
<path
class="mark"
d="M -1,0 L 1,0 L 1,10 L -1,10 z"
/>
></path>
</g>`,
},
{
@ -53,8 +53,8 @@ transform="scale(${scale})">
def: (scale) => `
<radialGradient id="snap-stud-grad" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"
transform="scale(${scale})">
<stop offset="30%" style="stop-color:rgb(235,235,235); stop-opacity:1"/>
<stop offset="80%" style="stop-color:rgb(100,100,100);stop-opacity:1" />
<stop offset="30%" style="stop-color:rgb(235,235,235); stop-opacity:1"></stop>
<stop offset="80%" style="stop-color:rgb(100,100,100);stop-opacity:1"></stop>
</radialGradient>`,
},
{
@ -64,14 +64,14 @@ transform="scale(${scale})">
transform="scale(${scale})">
<circle id="snap-stud-circle-edge" cx="0" cy="0" r="3.4"
style="stroke:#666;fill:#dddddd;stroke-width:0.3;"
/>
></circle>
<circle id="snap-stud-circle-middle" cx="0" cy="0" r="1.8"
style="stroke:none;fill:url(#snap-stud-grad);"
/>
></circle>
<path
id="snap-stud-lines" style="fill:none;stroke:#666; stroke-width:0.2;"
d="M -2,0 L -3,0 M 2,0 L 3,0 M 0,2 L 0,3 M 0,-2 L 0,-3 M 1.5,1.5 L 2.1,2.1 M -1.5,1.5 L -2.1,2.1 M -1.5,-1.5 L -2.1,-2.1 M 1.5,-1.5 L 2.1,-2.1"
/>
></path>
</g>`,
},
{
@ -81,14 +81,22 @@ transform="scale(${scale})">
transform="scale(${scale})">
<circle id="snap-socket-circle-edge" cx="0" cy="0" r="3.4"
style="stroke:#666;fill:#bbbbbb;stroke-width:0.3;"
/>
></circle>
<circle id="snap-socket-circle-middle" cx="0" cy="0" r="2"
style="stroke:#666;fill:#dddddd; stroke-width:0.4;"
/>
></circle>
<path
style="fill:none;stroke:#666; stroke-width:0.5;"
d="M -1.7,-1 L -1.7,1 M 1.7,-1 L 1.7,1" id="snap-socket-lines"
/>
></path>
</g>`,
},
{
name: 'eyelet',
def: `
<g id="eyelet">
<circle id="eyelet-circle" cx="0" cy="0" r="3.4" class="no-full stroke-mark mark" stroke-width="1" fill="none" stroke="currentColor">
</circle>
</g>`,
},
]

View file

@ -1,53 +1,127 @@
// Export macros
export const crossboxMacros = {
crossbox: function (so, { points, Point, paths, Path, getId }) {
const id = getId()
const shiftFraction = 0.1
points[id + '_boxTopLeft'] = so.from.copy()
points[id + '_boxBottomRight'] = so.to.copy()
points[id + '_boxTopRight'] = new Point(so.to.x, so.from.y)
points[id + '_boxBottomLeft'] = new Point(so.from.x, so.to.y)
import { getIds } from './utils.mjs'
points[id + '_topCrossTL'] = points[id + '_boxTopLeft'].shiftFractionTowards(
points[id + '_boxBottomRight'],
shiftFraction
)
points[id + '_topCrossTR'] = points[id + '_boxTopRight'].shiftFractionTowards(
points[id + '_boxBottomLeft'],
shiftFraction
)
points[id + '_topCrossBL'] = points[id + '_boxBottomLeft'].shiftFractionTowards(
points[id + '_boxTopRight'],
shiftFraction
)
points[id + '_topCrossBR'] = points[id + '_boxBottomRight'].shiftFractionTowards(
points[id + '_boxTopLeft'],
shiftFraction
)
paths[id + 'crossBox'] = new Path()
.move(points[id + '_boxTopLeft'])
.line(points[id + '_boxTopRight'])
.line(points[id + '_boxBottomRight'])
.line(points[id + '_boxBottomLeft'])
.line(points[id + '_boxTopLeft'])
.close()
.attr('class', 'lining dotted stroke-sm')
paths[id + '_topCross'] = new Path()
.move(points[id + '_topCrossTL'])
.line(points[id + '_topCrossBR'])
.line(points[id + '_topCrossTR'])
.line(points[id + '_topCrossBL'])
.line(points[id + '_topCrossTL'])
.line(points[id + '_topCrossTR'])
.move(points[id + '_topCrossBR'])
.line(points[id + '_topCrossBL'])
.attr('class', 'lining dotted stroke-sm')
if (typeof so.text === 'string') {
points[id + 'textAnchor'] = points[id + '_boxTopLeft']
.shiftFractionTowards(points[id + '_boxBottomRight'], 0.5)
.attr('data-text', so.text)
.attr('data-text-class', 'center')
}
/*
* Defaults for the title macro
*/
const macroDefaults = {
classes: {
box: 'lining dotted stroke-sm',
cross: 'lining dotted stroke-sm',
text: 'center fill-lining',
},
id: 'crossbox',
offset: 0.1,
text: '',
}
/*
* The rmcrossbox macro
*/
const rmcrossbox = function (id = macroDefaults.id, { paths, points, store, part }) {
const both = store.get(['parts', part.name, 'macros', 'title', '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 crossbox macro
*/
const crossbox = function (config, { points, Point, paths, Path, complete, store, log, 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,
...config,
classes: macroDefaults.classes,
}
if (config.classes) mc.classes = { ...mc.classes, ...config.classes }
/*
* Make sure mc.topLeft and mc.bottomRight are Point instances
*/
if (!mc.topLeft || typeof mc.topLeft.attr !== 'function') {
log.warn(`Crossbox macro called without a valid topLeft point. Using (0,0) for topLeft.`)
mc.topLeft = new Point(0, 0)
}
if (!mc.bottomRight || typeof mc.bottomRight.attr !== 'function') {
log.warn(
`Crossbox macro called without a valid bottomRight point. Using (666,666) for bottomRight.`
)
mc.bottomRight = new Point(666, 666)
}
/*
* Get the list of IDs
*/
const flatIds = getIds(['box', 'cross', 'text'], mc.id, 'crossbox')
const ids = {
paths: {
box: flatIds.box,
cross: flatIds.cross,
},
points: { text: flatIds.text },
}
/*
* Calculate the cross offset as [offset]% of the shortest side of the box
*/
const offset =
Math.abs(mc.topLeft.dx(mc.bottomRight)) > Math.abs(mc.topLeft.dy(mc.bottomRight))
? Math.abs(mc.topLeft.dx(mc.bottomRight)) * mc.offset
: Math.abs(mc.topLeft.dy(mc.bottomRight)) * mc.offset
/*
* Draw the box
*/
paths[ids.paths.box] = new Path()
.move(mc.topLeft)
.line(new Point(mc.topLeft.x, mc.bottomRight.y))
.line(mc.bottomRight)
.line(new Point(mc.bottomRight.x, mc.topLeft.y))
.line(mc.topLeft)
.close()
.attr('class', mc.classes.box)
/*
* Draw the cross
*/
paths[ids.paths.cross] = new Path()
.move(mc.topLeft.shift(315, offset))
.line(new Point(mc.bottomRight.x, mc.topLeft.y).shift(225, offset))
.line(mc.bottomRight.shift(135, offset))
.line(new Point(mc.topLeft.x, mc.bottomRight.y).shift(45, offset))
.line(mc.topLeft.shift(315, offset))
.line(mc.bottomRight.shift(135, offset))
.move(new Point(mc.bottomRight.x, mc.topLeft.y).shift(225, offset))
.line(new Point(mc.topLeft.x, mc.bottomRight.y).shift(45, offset))
.attr('class', mc.classes.box)
/*
* If there is text, add it
*/
if (mc.text)
points[ids.points.text] = mc.topLeft
.shiftFractionTowards(mc.bottomRight, 0.5)
.addText(mc.text, mc.classes.text)
else delete ids.points.text
/*
* Store all IDs in the store so we can remove this macro with rmtitle
* Just make sure to keep points and paths apart
*/
store.set(['parts', part.name, 'macros', 'title', 'ids', mc.id], ids)
return store.getMacroIds(mc.id, 'crossbox')
}
// Export macros
export const crossboxMacros = { crossbox, rmcrossbox }

View file

@ -1,5 +1,6 @@
export const cutlistStores = [
['cutlist.addCut', addCut],
['cutlist.setCut', setCut],
['cutlist.removeCut', removeCut],
['cutlist.setGrain', setGrain],
['cutlist.setCutOnFold', setCutOnFold],
@ -24,40 +25,51 @@ export const cutlistHooks = {
* @param {Store} store the Store
* @param {Object} so a set of cutting instructions for a material
* @param {number} so.cut = 2 the number of pieces to cut from the specified fabric
* @param {string} so.material = fabric the name of the material to cut from
* @param {string} so.from = fabric the name of the material to cut from
* @param {boolean} so.identical = false should even numbers of pieces be cut in the same direction or mirrored
* @param {boolean} so.bias = false should the pieces in these cutting instructions be cut on the bias
* @param {boolean} so.ignoreOnFold should these cutting instructions ignore any cutOnFold information set by the part
* @param {boolean} so.onBias = false should the pieces in these cutting instructions be cut on the bias
* @param {boolean} so.onFold = false should these cutting instructions ignore any cutOnFold information set by the part
*/
function addCut(store, so = {}) {
const { cut = 2, material = 'fabric', identical = false, bias = false, ignoreOnFold = false } = so
if (Array.isArray(so)) {
for (const cut of so) addCut(store, cut)
return store
}
const { cut = 2, from = 'fabric', identical = false, onBias = false, onFold = false } = so
const partName = store.get('activePart')
if (cut === false) {
if (material === false) store.unset(['cutlist', partName, 'materials'])
else store.unset(['cutlist', partName, 'materials', material])
if (from === false) store.unset(['cutlist', partName, 'materials'])
else store.unset(['cutlist', partName, 'materials', from])
return store
}
if (!(Number.isInteger(cut) && cut > -1)) {
store.log.error(`Tried to set cut to a value that is not a positive integer`)
return store
}
if (typeof material !== 'string') {
store.log.warning(`Tried to set material to a value that is not a string`)
if (typeof from !== 'string') {
store.log.warn(`Tried to set material to a value that is not a string`)
return store
}
const path = ['cutlist', partName, 'materials', material]
const path = ['cutlist', partName, 'materials', from]
const existing = store.get(path, [])
store.set(path, existing.concat({ cut, identical, bias, ignoreOnFold }))
store.set(path, existing.concat({ cut, identical, onBias, onFold }))
return store
}
/** Method to remove the cut info */
function removeCut(store, material = false) {
return addCut(store, { cut: false, material })
function removeCut(store, from = false) {
return addCut(store, { cut: false, from })
}
/** Method to add the grain info */
/** Method to set (remove + add) the cut info */
function setCut(store, so) {
removeCut(store)
return addCut(store, so)
}
/** Method to add the grain info (called by grainline and cutonfold macros) */
function setGrain(store, grain = false) {
const partName = store.get('activePart')
const path = ['cutlist', partName, 'grain']
@ -69,7 +81,7 @@ function setGrain(store, grain = false) {
return store.set(path, grain)
}
/** Method to add the cutOnFold info */
/** Method to add the cutOnFold info (called by cutonfold macro) */
function setCutOnFold(store, p1, p2) {
const partName = store.get('activePart')
const path = ['cutlist', partName, 'cutOnFold']

View file

@ -1,3 +1,20 @@
import { getIds } from './utils.mjs'
/*
* Defaults for the cutonfold macro
*/
const macroDefaults = {
classes: {
line: 'note',
text: 'center fill-note',
},
id: 'cutonfold',
grainline: false,
margin: 0.05,
offset: 15,
reverse: false,
}
// Export defs
export const cutonfoldDefs = [
{
@ -16,50 +33,83 @@ export const cutonfoldDefs = [
},
]
// Export macros
export const cutonfoldMacros = {
cutonfold: function (so, { points, paths, Path, complete, store, scale }) {
if (so === false) {
delete points.cutonfoldFrom
delete points.cutonfoldTo
delete points.cutonfoldVia1
delete points.cutonfoldVia2
delete paths.cutonfoldCutonfold
store.cutlist.setCutOnFold(false) // Restore default
return true
}
so = {
offset: 15,
margin: 5,
prefix: 'cutonfold',
...so,
}
// store in cutlist
store.cutlist.setCutOnFold(so.from, so.to)
if (so.grainline) store.cutlist.setGrain(so.from.angle(so.to))
if (complete) {
points[so.prefix + 'From'] = so.from.shiftFractionTowards(so.to, so.margin / 100)
points[so.prefix + 'To'] = so.to.shiftFractionTowards(so.from, so.margin / 100)
points[so.prefix + 'Via1'] = points[so.prefix + 'From']
.shiftTowards(so.from, so.offset * scale)
.rotate(-90, points[so.prefix + 'From'])
points[so.prefix + 'Via2'] = points[so.prefix + 'To']
.shiftTowards(so.to, so.offset * scale)
.rotate(90, points[so.prefix + 'To'])
const text = so.grainline ? 'cutOnFoldAndGrainline' : 'cutOnFold'
paths[so.prefix + 'Cutonfold'] = new Path()
.move(points[so.prefix + 'From'])
.line(points[so.prefix + 'Via1'])
.line(points[so.prefix + 'Via2'])
.line(points[so.prefix + 'To'])
.attr('class', 'note')
.attr('marker-start', 'url(#cutonfoldFrom)')
.attr('marker-end', 'url(#cutonfoldTo)')
.attr('data-text', text)
.attr('data-text-class', 'center fill-note')
}
},
/*
* The rmcutonfold macro
*/
const rmcutonfold = function (id = macroDefaults.id, { paths, store, part }) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', 'cutonfold', 'ids', id, 'paths'], {})
))
delete paths[pid]
}
/*
* The cutonfold macro
*/
const cutonfold = function (config, { paths, Path, complete, store, scale, log, Point, part }) {
/*
* Don't add a cutonfold indicator 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,
text: config.grainline
? 'plugin-annotations:cutOnFoldAndGrainline'
: 'plugin-annotations:cutOnFold',
...config,
classes: macroDefaults.classes,
}
if (config.classes) mc.classes = { ...mc.classes, ...config.classes }
/*
* Make sure mc.from and mc.to are Point instances
*/
if (!mc.from || typeof mc.from.attr !== 'function') {
log.warn(`Cutonfold macro called without a valid from point. Using (0,0) for from.`)
mc.from = new Point(0, 0)
}
if (!mc.to || typeof mc.to.attr !== 'function') {
log.warn(`Cutonfold macro called without a valid to point. Using (6660,666) for to.`)
mc.to = new Point(666, 666)
}
/*
* Store cutonfold and optional grainline angle for use in cutlist
*/
store.cutlist.setCutOnFold(mc.from, mc.to)
if (mc.grainline) store.cutlist.setGrain(mc.from.angle(mc.to))
/*
* Get the list of IDs
*/
const ids = getIds(['line'], mc.id, 'cutonfold')
/*
* Draw the path
*/
const from = mc.from.shiftFractionTowards(mc.to, mc.margin)
const to = mc.to.shiftFractionTowards(mc.from, mc.margin)
const via1 = from.shiftTowards(mc.from, mc.offset * scale).rotate(-90, from)
const via2 = to.shiftTowards(mc.to, mc.offset * scale).rotate(90, to)
paths[ids.line] = new Path().move(from).line(via1).line(via2).line(to)
if (mc.reverse) paths[ids.line] = paths[ids.line].reverse()
paths[ids.line] = paths[ids.line]
.attr('class', mc.classes.line)
.attr('marker-start', 'url(#cutonfoldFrom)')
.attr('marker-end', 'url(#cutonfoldTo)')
.addText(mc.text, mc.classes.text)
/*
* Store all IDs in the store so we can remove this macro with rmcutonfold
*/
store.set(['parts', part.name, 'macros', 'cutonfold', 'ids', mc.id, 'paths'], ids)
return store.getMacroIds(mc.id, 'cutonfold')
}
// Export macros
export const cutonfoldMacros = { cutonfold, rmcutonfold }

View file

@ -1,3 +1,5 @@
import { getIds } from './utils.mjs'
// Export defs
export const dimensionsDefs = [
{
@ -16,8 +18,73 @@ export const dimensionsDefs = [
},
]
const prefix = '__paperless'
/*
* Defaults for these macros
*/
const macroDefaults = {
text: false,
noStartMarker: false,
noEndMarker: false,
classes: {
line: 'mark',
leaders: 'mark dotted',
text: 'fill-mark center',
},
}
/*
* Higher-level methods to draw leaders for various types
*/
const leaders = {
hd: function hleader(so, type, props, id) {
let point
if (typeof so.y === 'undefined' || so[type].y === so.y) point = so[type]
else {
point = new props.Point(so[type].x, so.y)
drawLeader(props, so[type], point, id)
}
return point
},
vd: function vleader(so, type, props, id) {
let point
if (typeof so.x === 'undefined' || so[type].x === so.x) point = so[type]
else {
point = new props.Point(so.x, so[type].y)
drawLeader(props, so[type], point, id)
}
return point
},
ld: function lleader(so, type, props, id) {
let point, rot, other
if (type === 'from') {
rot = 1
other = 'to'
} else {
rot = -1
other = 'from'
}
if (typeof so.d === 'undefined') point = so[type]
else {
point = so[type].shiftTowards(so[other], so.d).rotate(90 * rot, so[type])
drawLeader(props, so[type], point, id)
}
return point
},
}
/*
* Low-level method to draw a leader
*/
function drawLeader({ paths, Path }, from, to, id) {
paths[id] = new Path().move(from).line(to).attr('class', 'mark dotted')
}
/*
* Low-level method to draw a dimension
*/
function drawDimension(from, to, so, { Path, units }) {
const dimension = new Path()
.move(from)
@ -25,136 +92,113 @@ function drawDimension(from, to, so, { Path, units }) {
.attr('class', 'mark')
.attr('data-text', so.text || units(from.dist(to)))
.attr('data-text-class', 'fill-mark center')
.attr('data-macro-id', so.id)
if (!so.noStartMarker) dimension.attributes.set('marker-start', 'url(#dimensionFrom)')
if (!so.noEndMarker) dimension.attributes.set('marker-end', 'url(#dimensionTo)')
return dimension
}
function drawLeader({ paths, Path }, from, to, id) {
paths[id] = new Path().move(from).line(to).attr('class', 'mark dotted')
}
/*
* This method handles all dimension macros
*/
const addDimension = (config, props, type) => {
/*
* Don't add a dimention when paperless is false, unless force is true
*/
if (!props.paperless && !config.force) return
function hleader(so, type, props, id) {
const { Point } = props
let point
if (typeof so.y === 'undefined' || so[type].y === so.y) {
point = so[type]
/*
* Merge macro defaults with user-provided config to create the macro config (mc)
*/
const mc = {
...macroDefaults[type],
id: type,
...config,
classes: macroDefaults.classes,
}
if (config.classes) mc.classes = { ...mc.classes, ...config.classes }
/*
* Get the list of IDs
*/
const ids = getIds(['line', 'from', 'to'], mc.id, type)
/*
* Draw the dimension
*/
if (type === 'pd') {
if (typeof mc.d === 'undefined') mc.d = 10 * props.scale
props.paths[ids.line] = mc.path
.offset(mc.d)
.attr('class', mc.classes.line)
.addText(mc.text || props.units(mc.path.length()), mc.classes.text)
if (!mc.noStartMarker)
props.paths[ids.line].attributes.set('marker-start', 'url(#dimensionFrom)')
if (!mc.noEndMarker) props.paths[ids.line].attributes.set('marker-end', 'url(#dimensionTo)')
drawLeader(props, mc.path.start(), props.paths[ids.line].start(), ids.from)
drawLeader(props, mc.path.end(), props.paths[ids.line].end(), ids.to)
} else {
point = new Point(so[type].x, so.y)
drawLeader(props, so[type], point, id)
props.paths[ids.line] = drawDimension(
leaders[type](mc, 'from', props, ids.from),
leaders[type](mc, 'to', props, ids.to),
mc,
props
)
}
return point
/*
* Store all IDs in the store so we can remove this macro with rm variants
*/
props.store.set(['parts', props.part.name, 'macros', type, 'ids', mc.id, 'paths'], ids)
return props.store.getMacroIds(mc.id, type)
}
function vleader(so, type, props, id) {
const { Point } = props
let point
if (typeof so.x === 'undefined' || so[type].x === so.x) {
point = so[type]
} else {
point = new Point(so.x, so[type].y)
drawLeader(props, so[type], point, id)
}
return point
/*
* This method handles the 'remove' part for all macros
*/
const removeDimension = function (id = macroDefaults.id, { paths, store, part }, type) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', type, 'ids', id, 'paths'], {})
))
delete paths[pid]
}
function lleader(so, type, props, id) {
let point, rot, other
if (type === 'from') {
rot = 1
other = 'to'
} else {
rot = -1
other = 'from'
/*
* This method removes all dimensions of a given type
*/
const removeDimensionType = function ({ paths, store, part }, type) {
for (const ids of Object.values(store.get(['parts', part.name, 'macros', type, 'ids'], {}))) {
for (const pid of Object.values(ids.paths)) delete paths[pid]
}
if (typeof so.d === 'undefined') {
point = so[type]
} else {
point = so[type].shiftTowards(so[other], so.d).rotate(90 * rot, so[type])
drawLeader(props, so[type], point, id)
}
return point
}
// Export macros
/*
* This method removes all dimensions
*/
const removeAllDimensions = function ({ macro }) {
macro('rmahd')
macro('rmald')
macro('rmavd')
macro('rmapd')
}
/*
* Export macros
*/
export const dimensionsMacros = {
// horizontal
hd: function (so, props) {
const { getId, paths } = props
const id = so.id || getId(prefix)
paths[id] = drawDimension(
hleader(so, 'from', props, id + '_ls'),
hleader(so, 'to', props, id + '_le'),
so,
props
)
},
// vertical
vd: function (so, props) {
const { getId, paths } = props
const id = so.id || getId(prefix)
paths[id] = drawDimension(
vleader(so, 'from', props, id + '_ls'),
vleader(so, 'to', props, id + '_le'),
so,
props
)
},
// linear
ld: function (so, props) {
const { getId, paths } = props
const id = so.id || getId(prefix)
paths[id] = drawDimension(
lleader(so, 'from', props, id + '_ls'),
lleader(so, 'to', props, id + '_le'),
so,
props
)
},
// path
pd: function (so, props) {
const { getId, paths, scale, units } = props
const id = so.id || getId(prefix)
if (typeof so.d === 'undefined') so.d = 10 * scale
const dimension = so.path
.offset(so.d)
.attr('class', 'mark')
.attr('data-text', so.text || units(so.path.length()))
.attr('data-text-class', 'fill-mark center')
if (!so.noStartMarker) dimension.attributes.set('marker-start', 'url(#dimensionFrom)')
if (!so.noEndMarker) dimension.attributes.set('marker-end', 'url(#dimensionTo)')
paths[id] = dimension
drawLeader(props, so.path.start(), dimension.start(), id + '_ls')
drawLeader(props, so.path.end(), dimension.end(), id + '_le')
},
// Remove dimension
rmd: function (so, props) {
const { paths } = props
if (paths[so.id]) delete this.paths[so.id]
if (paths[`${so.id}_ls`]) delete paths[`${so.id}_ls`]
if (paths[`${so.id}_le`]) delete paths[`${so.id}_le`]
if (Array.isArray(so.ids)) {
for (const id of so.ids) {
if (paths[id]) delete paths[id]
if (paths[`${id}_ls`]) delete paths[`${id}_ls`]
if (paths[`${id}_le`]) delete paths[`${id}_le`]
}
}
},
// Remove all dimensions (with standard prefix)
rmad: function (params, props) {
const toRemove = {
points: props.point,
paths: props.paths,
}
for (let type in toRemove) {
for (let id in props[type]) {
if (id.slice(0, prefix.length) === prefix) delete props[type][id]
}
}
},
hd: (config, props) => addDimension(config, props, 'hd'),
ld: (config, props) => addDimension(config, props, 'ld'),
vd: (config, props) => addDimension(config, props, 'vd'),
pd: (config, props) => addDimension(config, props, 'pd'),
rmhd: (id, props) => removeDimension(id, props, 'hd'),
rmld: (id, props) => removeDimension(id, props, 'ld'),
rmvd: (id, props) => removeDimension(id, props, 'vd'),
rmpd: (id, props) => removeDimension(id, props, 'pd'),
rmahd: (config, props) => removeDimensionType(props, 'hd'),
rmald: (config, props) => removeDimensionType(props, 'ld'),
rmavd: (config, props) => removeDimensionType(props, 'vd'),
rmapd: (config, props) => removeDimensionType(props, 'pd'),
rmad: (config, props) => removeAllDimensions(props),
}

View file

@ -0,0 +1,94 @@
const storeRoot = ['plugins', 'plugin-annotations', 'flags']
// This is also the order in which they will be displayed
export const flagTypes = ['error', 'warn', 'note', 'info', 'tip', 'fixme']
export const flagStores = [
['flag.info', (store, data) => flag('info', store, data)],
['flag.tip', (store, data) => flag('tip', store, data)],
['flag.note', (store, data) => flag('note', store, data)],
['flag.warn', (store, data) => flag('warn', store, data)],
['flag.fixme', (store, data) => flag('fixme', store, data)],
['flag.error', (store, data) => flag('error', store, data)],
['flag.preset', (store, preset) => flag('preset', store, preset)],
['unflag.info', (store, id) => unflag('info', store, id)],
['unflag.tip', (store, id) => unflag('tip', store, id)],
['unflag.note', (store, id) => unflag('note', store, id)],
['unflag.warn', (store, id) => unflag('warn', store, id)],
['unflag.fixme', (store, id) => unflag('fixme', store, id)],
['unflag.error', (store, id) => unflag('error', store, id)],
['unflag.preset', (store, preset) => unflag('preset', store, preset)],
]
/*
* Method that adds a flag to the store
*
* @param {type} string - The type of flag
* @param {store} object - The pattern store
* @param {data} object - The flag data
*/
function flag(type, store, data) {
// Load presets
if (type === 'preset' && presets[data]) {
data = presets[data]
type = data.type
}
if (data.msg) {
data.title = data.msg + '.t'
data.desc = data.msg + '.d'
delete data.msg
}
if (!data.id && !data.title) {
store.log.warn(`store.flag.${type} called without an id or title property`)
console.log(data)
return
}
store.set([...storeRoot, type, data.id ? data.id : data.title], data)
}
/*
* Method that removes a flag from the store
*
* @param {type} string - The type of flag
* @param {store} object - The pattern store
* @param {id} string - The flag id to remove
*/
function unflag(type, store, id) {
if (type === 'preset' && presets[id]) {
type = presets[id].type
id = presets[id].id || presets[id].title
}
store.unset([...storeRoot, type, id])
}
/*
* Available flag presets
*/
const presets = {
expandIsOff: {
type: 'tip',
title: 'flag:expandIsOff.t',
desc: 'flag:expandIsOff.d',
suggest: {
text: 'flag:enable',
icon: 'expand',
update: {
settings: ['expand', 1],
},
},
},
expandIsOn: {
type: 'tip',
title: 'flag:expandIsOn.t',
desc: 'flag:expandIsOn.d',
suggest: {
text: 'flag:disable',
icon: 'expand',
update: {
settings: ['expand', null],
},
},
},
}

View file

@ -1,3 +1,18 @@
import { getIds } from './utils.mjs'
/*
* Defaults for the grainline macro
*/
const macroDefaults = {
classes: {
line: 'note',
text: 'center fill-note',
},
id: 'grainline',
margin: 0.05,
text: 'plugin-annotations:grainline',
}
// Export defs
export const grainlineDefs = [
{
@ -16,37 +31,77 @@ export const grainlineDefs = [
},
]
const dflts = { text: 'grainline' }
/*
* The rmgrainline macro
*/
const rmgrainline = function (id = macroDefaults.id, { paths, store, part }) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', 'grainline', 'ids', id, 'paths'], {})
))
delete paths[pid]
}
/*
* The grainline macro
*/
const grainline = function (config = {}, { paths, Path, Point, complete, store, log, part }) {
/*
* Don't add a cutonfold indicator 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,
...config,
classes: macroDefaults.classes,
}
if (config.classes) mc.classes = { ...mc.classes, ...config.classes }
/*
* Make sure mc.from and mc.to are Point instances
*/
if (!mc.from || typeof mc.from.attr !== 'function') {
log.warn(`Grainline macro called without a valid from point. Using (0,0) for from.`)
mc.from = new Point(0, 0)
}
if (!mc.to || typeof mc.to.attr !== 'function') {
log.warn(`Grainline macro called without a valid to point. Using (666,666) for to.`)
mc.to = new Point(666, 666)
}
/*
* Store angle for use in cutlist
*/
store.cutlist.setGrain(mc.from.angle(mc.to))
/*
* Get the list of IDs
*/
const ids = getIds(['line'], mc.id, 'grainline')
/*
* Draw the path
*/
const from = mc.from.shiftFractionTowards(mc.to, 0.05)
const to = mc.to.shiftFractionTowards(mc.from, 0.05)
paths[ids.line] = new Path()
.move(from)
.line(to)
.attr('class', mc.classes.line)
.attr('marker-start', 'url(#grainlineFrom)')
.attr('marker-end', 'url(#grainlineTo)')
.addText(mc.text, mc.classes.text)
/*
* Store all IDs in the store so we can remove this macro with rmgrainline
*/
store.set(['parts', part.name, 'macros', 'grainline', 'ids', mc.id, 'paths'], ids)
return store.getMacroIds(mc.id, 'grainline')
}
// Export macros
export const grainlineMacros = {
grainline: function (so = {}, { points, paths, Path, complete, store }) {
if (so === false) {
delete points.grainlineFrom
delete points.grainlineTo
delete paths.grainline
if (store.cutlist?.setGrain) store.cutlist.setGrain(90) // Restoring default
return true
}
so = {
...dflts,
...so,
}
// store in cutlist
store.cutlist.setGrain(so.from.angle(so.to))
if (complete) {
points.grainlineFrom = so.from.shiftFractionTowards(so.to, 0.05)
points.grainlineTo = so.to.shiftFractionTowards(so.from, 0.05)
paths.grainline = new Path()
.move(points.grainlineFrom)
.line(points.grainlineTo)
.attr('class', 'note')
.attr('marker-start', 'url(#grainlineFrom)')
.attr('marker-end', 'url(#grainlineTo)')
.attr('data-text', so.text)
.attr('data-text-class', 'center fill-note')
}
},
}
export const grainlineMacros = { grainline, rmgrainline }

View file

@ -1,4 +1,6 @@
import { name, version } from '../data.mjs'
// i18n
import { i18n as i18nAnnotations } from '../i18n/index.mjs'
// Defs only
import { buttonsDefs } from './buttons.mjs'
import { logoDefs } from './logo.mjs'
@ -17,6 +19,9 @@ import { dimensionsMacros, dimensionsDefs } from './dimensions.mjs'
import { grainlineMacros, grainlineDefs } from './grainline.mjs'
import { pleatMacros, pleatDefs } from './pleat.mjs'
import { sewtogetherMacros, sewtogetherDefs } from './sewtogether.mjs'
// Only stores
import { flagStores } from './flag.mjs'
import { utilsStores } from './utils.mjs'
export const plugin = {
name,
@ -57,8 +62,9 @@ export const plugin = {
...sewtogetherMacros,
...titleMacros,
},
store: [...cutlistStores],
store: [...cutlistStores, ...flagStores, ...utilsStores],
}
export const annotationsPlugin = plugin
export const pluginAnnotations = plugin
export const i18n = i18nAnnotations

View file

@ -4,16 +4,16 @@ export const notchesDefs = [
name: 'notch',
def: (scale) => `
<g id="notch" transform="scale(${scale})">
<circle cy="0" cx="0" r="1.4" class="fill-note" />
<circle cy="0" cx="0" r="2.8" class="note" />
<circle cy="0" cx="0" r="1.4" class="fill-note"></circle>
<circle cy="0" cx="0" r="2.8" class="note"></circle>
</g>`,
},
{
name: 'bnotch',
def: (scale) => `
<g id="bnotch" transform="scale(${scale})">
<path d="M -1.1 -1.1 L 1.1 1.1 M 1.1 -1.1 L -1.1 1.1" class="note" />
<circle cy="0" cx="0" r="2.8" class="note" />
<path d="M -1.1 -1.1 L 1.1 1.1 M 1.1 -1.1 L -1.1 1.1" class="note"></path>
<circle cy="0" cx="0" r="2.8" class="note"></circle>
</g>`,
},
]

View file

@ -1,68 +1,118 @@
import { getIds } from './utils.mjs'
/*
* Defaults for the pleat macro
*/
const macroDefaults = {
classes: {
arrow: 'note',
from: 'note',
to: 'note dashed',
},
id: 'pleat',
margin: 35,
reverse: false,
}
// Export defs
export const pleatDefs = [
{
name: 'pleatTo',
name: 'pleat',
def: (scale) => `
<marker orient="auto" refY="0" refX="0" id="pleatTo" style="overflow:visible;" markerWidth="12" markerHeight="8">
<path class="note fill-note" d="M 0,0 L -12,-4 C -10,-2 -12,2 -12,4 z" transform="scale(${scale})"/>
<marker id="pleatTo" markerWidth="10" markerHeight="6" orient="auto" refY="3" refX="10">
<path d="M 10,3 L 0,0 C 2,2 2,4 0,6 z" class="fill-note note" transform="scale(${scale})" />
</marker>
`,
},
]
// Export macros
export const pleatMacros = {
pleat: function (so, { points, paths, Path, complete, scale }) {
if (so === false) {
delete points.pleatFrom
delete points.pleatFromIn
delete points.pleatTo
delete points.pleatToIn
delete paths.pleatTo
delete paths.pleatFrom
delete paths.pleatArrow
return true
}
so = {
margin: 35,
prefix: 'pleat',
reverse: false,
...so,
}
if (complete) {
points[so.prefix + 'From'] = so.from.copy()
points[so.prefix + 'To'] = so.to.copy()
points[so.prefix + 'FromIn'] = points[so.prefix + 'From'].shift(
so.from.shiftTowards(so.to, 0.1).angle(so.from) + 270,
so.margin * scale
)
points[so.prefix + 'ToIn'] = points[so.prefix + 'To'].shift(
so.from.shiftTowards(so.to, 0.1).angle(so.to) + 90,
so.margin * scale
)
paths[so.prefix + 'PleatFrom'] = new Path()
.move(points[so.prefix + 'From'])
.line(points[so.prefix + 'FromIn'])
.attr('class', 'note' + (so.reverse ? ' dashed' : ''))
paths[so.prefix + 'PleatTo'] = new Path()
.move(points[so.prefix + 'To'])
.line(points[so.prefix + 'ToIn'])
.attr('class', 'note' + (so.reverse ? '' : ' dashed'))
paths[so.prefix + 'PleatArrow'] = new Path()
.move(
points[so.prefix + (so.reverse ? 'To' : 'From')].shiftFractionTowards(
points[so.prefix + (so.reverse ? 'ToIn' : 'FromIn')],
0.25
)
)
.line(
points[so.prefix + (so.reverse ? 'From' : 'To')].shiftFractionTowards(
points[so.prefix + (so.reverse ? 'FromIn' : 'ToIn')],
0.25
)
)
.attr('class', 'note')
.attr('marker-end', 'url(#pleatTo)')
}
},
/*
* The rmpleat macro
*/
const rmpleat = function (id = macroDefaults.id, { paths, store, part }) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', 'pleat', 'ids', id, 'paths'], {})
))
delete paths[pid]
}
/*
* The pleat macro
*/
const pleat = function (config, { paths, Path, log, Point, complete, scale, store, part }) {
/*
* Don't add a pleat 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,
...config,
classes: macroDefaults.classes,
}
if (config.classes) mc.classes = { ...mc.classes, ...config.classes }
/*
* Make sure mc.from and mc.to are Point instances
*/
if (!mc.from || typeof mc.from.attr !== 'function') {
log.warn(`Pleat macro called without a valid from point. Using (0,0) for from.`)
mc.from = new Point(0, 0)
}
if (!mc.to || typeof mc.to.attr !== 'function') {
log.warn(`Pleat macro called without a valid to point. Using (666,666) for to.`)
mc.to = new Point(666, 666)
}
/*
* Get the list of IDs
* Initialize the verticle cadence
*/
const ids = getIds(['from', 'to', 'arrow'], mc.id, 'pleat')
const toIn = mc.to.shift(mc.from.shiftTowards(mc.to, 0.1).angle(mc.to) + 90, mc.margin * scale)
const fromIn = mc.from.shift(
mc.from.shiftTowards(mc.to, 0.1).angle(mc.from) + 270,
mc.margin * scale
)
/*
* Pleat line from
*/
paths[ids.from] = new Path()
.move(mc.from)
.line(fromIn)
.attr('class', mc.reverse ? mc.classes.to : mc.classes.from)
/*
* Pleat line to
*/
paths[ids.to] = new Path()
.move(mc.to)
.line(toIn)
.attr('class', mc.reverse ? mc.classes.from : mc.classes.to)
/*
* Pleat line arrow
*/
paths[ids.arrow] = mc.reverse
? new Path()
.move(mc.to.shiftFractionTowards(toIn, 0.25))
.line(mc.from.shiftFractionTowards(toIn, 0.25))
: new Path()
.move(mc.from.shiftFractionTowards(fromIn, 0.25))
.line(mc.to.shiftFractionTowards(fromIn, 0.25))
paths[ids.arrow].attr('class', mc.classes.arrow).attr('marker-end', 'url(#pleatTo)')
/*
* Store all IDs in the store so we can remove this macro with rmpleat
*/
store.set(['parts', part.name, 'macros', 'pleat', 'ids', mc.id, 'paths'], ids)
return store.getMacroIds(mc.id, 'pleat')
}
// Export macros
export const pleatMacros = { pleat, rmpleat }

View file

@ -1,34 +1,31 @@
// 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-xs',
link: 'text-sm fill-note bold',
metric: 'text-xs center',
imperial: 'text-xs center',
imperialBox: 'scalebox imperial fill-current',
metricBox: 'scalebox metric fill-bg',
},
lead: 'FreeSewing',
link: 'FreeSewing.org/patrons/join',
text: 'plugin-annotations:supportFreeSewingBecomeAPatron',
title: false,
}
// 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 +36,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,227 +48,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 = points.__scaleboxImperialTopLeft
.shift(-90, 7 * scale)
.shift(0, 2 * 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
.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, log, 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.at = 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)
text[pid].attr(
'data-text-transform',
`rotate(${mc.rotate * -1}, ${text[pid].x}, ${text[pid].y})`,
true
)
}
}
/*
* Draw the imperial box
*/
paths[ids.imperial] = new Path()
.addClass(mc.classes.imperialBox)
.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()
.addClass(mc.classes.metricBox)
.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, mc.classes.lead)
/*
* 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 || 'plugin-annotations:noName'
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(mc.text, mc.classes.text)
/*
* Add link text to the part points
*/
points[ids.textLink] = text.link.addText(mc.link, mc.classes.link).attr('data-text-lineheight', 4)
/*
* Add metric instructions text to the part points
*/
points[ids.textMetric] = text.metric
.attr('data-text', 'plugin-annotations: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', 'plugin-annotations: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,
},
})
return store.getMacroIds(mc.id, 'scalebox')
}
/*
* The miniscale macro
*/
const miniscale = function (
config,
{ points, paths, scale, Point, Path, part, complete, log, 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.at = 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'),
}

View file

@ -1,93 +1,133 @@
import { getIds } from './utils.mjs'
/*
* Defaults for the sewtogether macro
*/
const macroDefaults = {
classes: {
curve: 'dotted note stroke-sm',
hinge: 'note dotted stroke-sm',
text: 'center fill-note text-xs',
},
id: 'sewtogether',
force: false,
text: 'plugin-annotations:sewTogether',
}
// Export defs
export const sewtogetherDefs = [
{
name: 'sewTogetherStart',
def: (scale) => `
<marker id="sewTogetherStart" markerWidth="8" markerHeight="8" style="overflow:visible;" orient="auto" refX="0" refY="0">
<path class="note stroke-sm" d="M 8,4 L 0,0 8,-4" transform="scale(${scale})"/>
<marker id="sewTogetherStart" markerWidth="10" markerHeight="6" orient="auto" refX="1" refY="2">
<path d="M 0,2 L 6,0 C 5,1 5,3 6,4 z" class="fill-note note" transform="scale(${scale})" />
</marker>`,
},
{
name: 'sewTogetherEnd',
def: (scale) => `
<marker id="sewTogetherEnd" markerWidth="8" markerHeight="8" style="overflow:visible;" orient="auto" refX="0" refY="0">
<path class="note stroke-sm" d="M -8,-4 L 0,0 -8,4" transform="scale(${scale})"/>
<marker id="sewTogetherEnd" markerWidth="10" markerHeight="6" orient="auto" refX="6" refY="2">
<path d="M 6,2 L 0,0 C 1,1 1,3 0,4 z" class="fill-note note" transform="scale(${scale})" />
</marker>`,
},
{
name: 'sewTogetherCross',
def: (scale) => `
<marker id="sewTogetherCross" markerWidth="8" markerHeight="8" style="overflow:visible;" orient="auto" refX="0" refY="0">
<path d="M -4,-4 L 4,4 M 4,-4 L -4,4" class="note stroke-sm" transform="scale(${scale})"/>
<marker id="sewTogetherCross" markerWidth="5" markerHeight="5" orient="auto" refX="2.5" refY="2.5">
<path d="M 0,0 L 5,5 M 5,0 L 0,5" class="note" transform="scale(${scale})" />
</marker>`,
},
]
// Export macros
export const sewtogetherMacros = {
sewTogether: function (so, { points, paths, Path, complete, sa }) {
if (so === false) {
delete points.sewtogetherFrom
delete points.sewtogetherFromCp
delete points.sewtogetherMiddle
delete points.sewtogetherTo
delete points.sewtogetherHinge
delete points.sewtogetherToCp
delete paths.sewtogetherSewTogetherHinge
delete paths.sewtogetherSewTogether
return true
}
so = {
prefix: 'sewtogether',
...so,
}
if (complete) {
if (null == so.middle) {
so.middle = so.from.shiftFractionTowards(so.to, 0.5)
}
points[so.prefix + 'From'] = so.from.copy()
points[so.prefix + 'Middle'] = so.middle.copy()
points[so.prefix + 'To'] = so.to.copy()
points[so.prefix + 'FromCp'] = points[so.prefix + 'From'].shift(
points[so.prefix + 'From'].angle(points[so.prefix + 'Middle']) + 90,
points[so.prefix + 'From'].dist(points[so.prefix + 'Middle']) / 1.5
)
points[so.prefix + 'ToCp'] = points[so.prefix + 'To'].shift(
points[so.prefix + 'To'].angle(points[so.prefix + 'Middle']) - 90,
points[so.prefix + 'To'].dist(points[so.prefix + 'Middle']) / 1.5
)
if (so.hinge) {
points[so.prefix + 'Hinge'] = points[so.prefix + 'Middle'].shift(
points[so.prefix + 'Middle'].angle(points[so.prefix + 'To']) +
Math.abs(
points[so.prefix + 'Middle'].angle(points[so.prefix + 'From']) -
points[so.prefix + 'Middle'].angle(points[so.prefix + 'To'])
) /
2 +
(sa ? 180 : 0),
sa
? sa
: Math.min(
points[so.prefix + 'From'].dist(points[so.prefix + 'Middle']),
points[so.prefix + 'From'].dist(points[so.prefix + 'Middle'])
) / 4
)
paths[so.prefix + 'SewTogetherHinge'] = new Path()
.move(points[so.prefix + 'Middle'])
.line(points[so.prefix + 'Hinge'])
.attr('marker-start', 'url(#sewTogetherCross)')
.attr('class', 'dotted note stroke-sm')
}
paths[so.prefix + 'SewTogether'] = new Path()
.move(points[so.prefix + 'From'])
.curve(points[so.prefix + 'FromCp'], points[so.prefix + 'ToCp'], points[so.prefix + 'To'])
.attr('class', 'dotted note stroke-sm')
.attr('marker-start', 'url(#sewTogetherStart)')
.attr('marker-end', 'url(#sewTogetherEnd)')
.attr('data-text', 'sewTogether')
.attr('data-text-class', 'center fill-note text-xs')
}
},
/*
* The rmsewtogether macro
*/
const rmsewtogether = function (id = macroDefaults.id, { paths, store, part }) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', 'sewtogether', 'ids', id, 'paths'], {})
))
delete paths[pid]
}
/*
* The sewtogether macro
*/
const sewtogether = function (config, { paths, Path, log, Point, complete, sa, store, 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,
...config,
classes: macroDefaults.classes,
}
if (config.classes) mc.classes = { ...mc.classes, ...config.classes }
/*
* Make sure mc.from and mc.to are Point instances
*/
if (!mc.from || typeof mc.from.attr !== 'function') {
log.warn(`Sewtogether macro called without a valid from point. Using (0,0) for from.`)
mc.from = new Point(0, 0)
}
if (!mc.to || typeof mc.to.attr !== 'function') {
log.warn(`Sewtogether macro called without a valid to point. Using (666,666) for to.`)
mc.to = new Point(666, 666)
}
/*
* Ensure we have a middle point
*/
if (!mc.middle) mc.middle = mc.from.shiftFractionTowards(mc.to, 0.5)
/*
* Get the list of IDs
* Initialize the verticle cadence
*/
const ids = getIds(['curve', 'hinge'], mc.id, 'sewtogether')
/*
* Draw the curve
*/
const fromCp = mc.from.shift(mc.from.angle(mc.middle) + 90, mc.from.dist(mc.middle) / 1.5)
const toCp = mc.to.shift(mc.to.angle(mc.middle) - 90, mc.to.dist(mc.middle) / 1.5)
paths[ids.curve] = new Path()
.move(mc.from)
.curve(fromCp, toCp, mc.to)
.attr('class', mc.classes.curve)
.attr('marker-start', 'url(#sewTogetherStart)')
.attr('marker-end', 'url(#sewTogetherEnd)')
.addText(mc.text, mc.classes.text)
/*
* Draw the hinge, if needed
*/
if (mc.hinge) {
const hinge = mc.middle.shift(
mc.middle.angle(mc.to) +
Math.abs(mc.middle.angle(mc.from) - mc.middle.angle(mc.to)) / 2 +
(sa ? 180 : 0),
sa ? sa : mc.from.dist(mc.middle) / 4
)
paths[ids.hinge] = new Path()
.move(mc.middle)
.line(hinge)
.attr('marker-start', 'url(#sewTogetherCross)')
.attr('class', mc.classes.hinge)
} else delete ids.hinge
/*
* Store all IDs in the store so we can remove this macro with rmsewtogether
*/
store.set(['parts', part.name, 'macros', 'sewtogether', 'ids', mc.id, 'paths'], ids)
return store.getMacroIds(mc.id, 'sewtogether')
}
// Export macros
export const sewtogetherMacros = { sewtogether, rmsewtogether }

View file

@ -1,108 +1,241 @@
const titleMacro = function (so, { points, scale, locale, store, part }) {
const prefix = so.prefix || ''
let overwrite = !so.append
import { getIds } from './utils.mjs'
// Passing `false` will remove the title
if (so === false || overwrite) {
for (const id of [
`_${prefix}_titleNr`,
`_${prefix}_titleName`,
`_${prefix}_titlePattern`,
`_${prefix}_titleFor`,
`_${prefix}_exportDate`,
])
delete points[id]
/*
* Defaults for the title macro
*/
const macroDefaults = {
align: 'left',
append: false,
cutlist: true,
dy: 8,
id: 'title',
force: false,
nr: 1,
rotation: 0,
scale: 1,
title: 'plugin-annotations:noName',
classes: {
cutlist: 'text-md fill-current',
date: 'text-sm fill-current',
for: 'fill-current font-bold',
name: 'fill-note',
nr: 'text-4xl fill-note font-bold',
title: 'text-lg fill-current font-bold',
},
}
if (so === false) return true
/*
* Removing all this is easy as all IDs are available in the store
* and all we need to remove are points.
*/
const removeTitleMacro = function (id = macroDefaults.id, { points, store, part }) {
for (const pid of Object.values(
store.get(['parts', part.name, 'macros', 'title', 'ids', id, 'points'], {})
))
delete points[pid]
}
/*
* The title macro
*/
const addTitleMacro = function (
config,
{ Point, points, scale, locale, store, part, log, complete }
) {
/*
* 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,
...config,
classes: macroDefaults.classes,
}
if (config.classes) mc.classes = { ...mc.classes, ...config.classes }
/*
* Take global scale setting into account
*/
mc.scale = mc.scale * scale
/*
* Make sure mc.at is a Point so we can anchor the title
*/
if (!mc.at || typeof mc.at.attr !== 'function') {
log.warn(`Title macro called without a valid anchor point. Anchoring title at (0,0).`)
mc.at = new Point(0, 0)
}
const transform = function (anchor) {
const cx = anchor.x - so.scale * anchor.x
const cy = anchor.y - so.scale * anchor.y
return `matrix(${so.scale}, 0, 0, ${so.scale}, ${cx}, ${cy}) rotate(${so.rotation} ${anchor.x} ${anchor.y})`
}
let shift = 8
const nextPoint = (text, textClass, shiftAmt = shift) => {
const newPoint = so.at.shift(-90 - so.rotation, shiftAmt * so.scale).addText(text, textClass)
newPoint.attr('data-text-transform', transform(newPoint))
return newPoint
}
const defaults = {
scale: 1,
rotation: 0,
cutlist: true,
/*
* Make sure mc.align is a valid alignment
*/
if (!['left', 'right', 'center'].includes(mc.align)) {
log.warn(`Title macro called with invalid alignement (${mc.align}). Left-aligning title.`)
mc.align = 'left'
}
so = { ...defaults, ...so }
so.scale = so.scale * scale
/*
* Calculate the transform only once
*/
const transform =
'matrix(' +
`${mc.scale}, 0, 0, ${mc.scale}, ` +
`${mc.at.x - mc.scale * mc.at.x}, ` +
`${mc.at.y - mc.scale * mc.at.y}` +
`) rotate(${mc.rotation} ${mc.at.x} ${mc.at.y})`
points[`_${prefix}_titleNr`] = so.at
.clone()
.attr('data-text', so.nr, overwrite)
.attr('data-text-class', 'text-4xl fill-note font-bold')
.attr('data-text-transform', transform(so.at))
/*
* Get the list of IDs
* Initialize the verticle cadence
*/
const ids = getIds(['cutlist', 'date', 'for', 'name', 'nr', 'title'], mc.id, 'title')
if (so.title) {
points[`_${prefix}_titleName`] = nextPoint(so.title, 'text-lg fill-current font-bold')
shift += 8
}
let shift = mc.dy
// Cut List instructions
const partCutlist = store.get(['cutlist', part.name])
// if there's a cutlist and it should be included
if (so.cutlist && partCutlist?.materials) {
// get the default cutonfold
const cutonfold = partCutlist.cutOnFold
// each material
for (const material in partCutlist.materials) {
// each set of instructions
partCutlist.materials[material].forEach(({ cut, identical, bias, ignoreOnFold }, c) => {
// make a new point for this set of instructions
const cutPoint = nextPoint('plugin:cut', 'text-md fill-current').addText(cut)
/*
* Title: nr
*/
if (typeof mc.nr !== 'undefined') {
points[ids.nr] = mc.at
.clone()
.attr('data-text', mc.nr, mc.append ? false : true)
.attr('data-text-class', `${mc.classes.nr} ${mc.align}`)
.attr('data-text-transform', transform)
.attr('data-render-always', 1) // Render even when outside the part bounding box
store.set(['partNumbers', part.name], mc.nr)
} else delete ids.nr
// if they're not identical, add that to the point's text
if (!identical && cut > 1) cutPoint.addText('plugin:mirrored')
/*
* Title: title
*/
if (mc.title) {
points[ids.title] = mc.at
.clone()
.shift(-90, shift)
.attr('data-text', mc.title, mc.append ? false : true)
.attr('data-text-class', `${mc.classes.title} ${mc.align}`)
.attr('data-text-transform', transform)
.attr('data-render-always', 1) // Render even when outside the part bounding box
shift += mc.dy
store.set(['partTitles', part.name], mc.title)
} else delete ids.title
// if they should be cut on the fold add that, with bias or without
if (cutonfold && !ignoreOnFold)
cutPoint.addText(bias ? 'plugin:onFoldAndBias' : 'plugin:onFoldLower')
// otherwise if they should be on the bias, say so
else if (bias) cutPoint.addText('plugin:onBias')
/*
* Title: cutlist
*/
if (mc.cutlist) {
/*
* Get cutlist instructions from the store, only proceed if the list is available
*/
const partCutlist = store.get(['cutlist', part.name], null)
if (partCutlist?.materials) {
/*
* Iterate over materials
*/
for (const [material, instructions] of Object.entries(partCutlist.materials)) {
instructions.forEach(({ cut, identical, onBias, onFold }, c) => {
/*
* Create point
*/
const id = `${ids.cutlist}_${material}_${c}`
ids[`cutlist_${material}_${c}`] = id
points[id] = mc.at
.clone()
.shift(-90, shift)
.attr('data-text', 'plugin-annotations:cut')
.attr('data-text-class', `${mc.classes.cutlist} ${mc.align}`)
.attr('data-text-transform', transform)
.attr('data-render-always', 1) // Render even when outside the part bounding box
.addText(cut)
shift += mc.dy
// add 'from' the material
cutPoint.addText('plugin:from').addText('plugin:' + material)
/*
* Add instructions if parts are mirrored
*/
if (!identical && cut > 1) points[id].addText('plugin-annotations:mirrored')
// save and shift
points[`_${prefix}_titleCut_${material}_${c}`] = cutPoint
shift += 8
})
/*
* Add instructions if parts are cut on fold
*/
if (onFold)
points[id].addText(
onBias ? 'plugin-annotations:onFoldAndBias' : 'plugin-annotations:onFold'
)
/*
* Add instructions if parts on on bias
*/ else if (onBias) points[id].addText('plugin-annotations:onBias')
/*
* Add 'from' (material) text
*/
points[id].addText('plugin-annotations:from').addText('plugin-annotations:' + material)
})
}
}
}
} else delete ids.cutlist
let name = store.data?.name || 'No Name'
name = name.replace('@freesewing/', '')
name += ' v' + (store.data?.version || 'No Version')
points[`_${prefix}_titlePattern`] = nextPoint(name, 'fill-note')
/*
* Title: Design name
*/
points[ids.name] = mc.at
.clone()
.shift(-90, shift)
.attr(
'data-text',
`${(store.data?.name || 'plugin-annotations:noName').replace('@freesewing/', '')} v${
store.data?.version || 'plugin-annotations:noVersion'
}`
)
.attr('data-text-class', `${mc.classes.name} ${mc.align}`)
.attr('data-text-transform', transform)
.attr('data-render-always', 1) // Render even when outside the part bounding box
shift += mc.dy
/*
* Title: For (measurements set)
*/
if (store.data.for) {
shift += 8
points[`_${prefix}_titleFor`] = nextPoint(`( ${store.data.for} )`, 'fill-current font-bold')
}
shift += 6
const now = new Date()
let hours = now.getHours()
let mins = now.getMinutes()
if (hours < 10) hours = `0${hours}`
if (mins < 10) mins = `0${mins}`
const exportDate = now.toLocaleDateString(locale || 'en', {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
})
points[`_${prefix}_exportDate`] = nextPoint(`${exportDate}@ ${hours}:${mins}`, 'text-sm')
points[ids.for] = mc.at
.shift(-90, shift)
.attr('data-text', `(${store.data.for})`)
.attr('data-text-class', `${mc.classes.for} ${mc.align}`)
.attr('data-text-transform', transform)
.attr('data-render-always', 1) // Render even when outside the part bounding box
shift += mc.dy
} else delete ids.for
/*
* Title: Date
*/
points[ids.date] = mc.at
.shift(-90, shift)
.attr(
'data-text',
new Date().toLocaleString(locale || 'en', {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
})
)
.attr('data-text-class', `${mc.classes.date} ${mc.align}`)
.attr('data-text-transform', transform)
.attr('data-render-always', 1) // Render even when outside the part bounding box
/*
* Store all IDs in the store so we can remove this macro with rmtitle
*/
store.set(['parts', part.name, 'macros', 'title', 'ids', mc.id, 'points'], ids)
return store.getMacroIds(mc.id, 'title')
}
// Export macros
export const titleMacros = { title: titleMacro }
export const titleMacros = {
title: addTitleMacro,
rmtitle: removeTitleMacro,
}

View file

@ -0,0 +1,27 @@
/*
* Helper method to get the various IDs for a macro
*/
export const getIds = (keys, id, macroName) => {
const ids = {}
for (const key of keys) ids[key] = `__macro_${macroName}_${id}_${key}`
return ids
}
/*
* Helper method to get an existing macro id
*/
const getIdsFromStore = (store, id, macroName, partName = false) => {
if (!partName) partName = store.activePart
const data = store.get(['parts', partName, 'macros', macroName, 'ids', id])
return data ? data : false
}
/*
* Add these to the store
*/
export const utilsStores = [
['createMacroIds', (store, keys, id, macroName) => getIds(keys, id, macroName)],
['getMacroIds', getIdsFromStore],
]