1
0
Fork 0
freesewing/packages/core/src/svg.mjs

475 lines
13 KiB
JavaScript
Raw Normal View History

import { Attributes } from './attributes.mjs'
2022-09-18 15:11:10 +02:00
import { __addNonEnumProp, round } from './utils.mjs'
2022-08-31 20:21:27 +02:00
import { version } from '../data.mjs'
2022-09-18 15:11:10 +02:00
//////////////////////////////////////////////
// CONSTRUCTOR //
//////////////////////////////////////////////
/**
* Constructor for an Svg
*
* @constructor
* @param {Patern} pattern - The Pattern object to render
* @return {Svg} this - The Path instance
*/
export function Svg(pattern) {
2022-09-10 18:45:57 +02:00
// Non-enumerable properties
2022-09-18 15:11:10 +02:00
__addNonEnumProp(this, 'openGroups', [])
__addNonEnumProp(this, 'freeId', 0)
__addNonEnumProp(this, 'prefix', '<?xml version="1.0" encoding="UTF-8" standalone="no"?>')
2022-09-10 18:45:57 +02:00
// Enumerable properties
2019-08-03 15:03:33 +02:00
this.pattern = pattern // Needed to expose pattern to hooks
this.attributes = new Attributes()
this.attributes.add('xmlns', 'http://www.w3.org/2000/svg')
this.attributes.add('xmlns:svg', 'http://www.w3.org/2000/svg')
this.attributes.add('xmlns:xlink', 'http://www.w3.org/1999/xlink')
2022-09-19 18:04:47 +02:00
this.attributes.add('xml:lang', pattern?.settings?.[0]?.locale || 'en')
2019-08-03 15:03:33 +02:00
this.attributes.add('xmlns:freesewing', 'http://freesewing.org/namespaces/freesewing')
this.attributes.add('freesewing', version)
this.layout = {}
this.body = ''
this.style = ''
this.defs = ''
2018-07-23 20:14:32 +02:00
}
2018-07-23 11:12:06 +00:00
2022-09-18 15:11:10 +02:00
//////////////////////////////////////////////
// PUBLIC METHODS //
//////////////////////////////////////////////
2018-08-07 15:23:37 +02:00
2022-09-18 15:11:10 +02:00
/**
* Renders a drafted Pattern as SVG
*
* @param {Pattern} pattern - The pattern to render
* @return {string} svg - The rendered SVG output
*/
2022-09-19 18:04:47 +02:00
Svg.prototype.render = function () {
this.idPrefix = this.pattern?.settings?.[0]?.idPrefix || 'fs-'
2022-09-18 15:11:10 +02:00
this.__runHooks('preRender')
2022-09-19 18:04:47 +02:00
this.pattern.__runHooks('postLayout')
if (!this.pattern.settings[0].embed) {
this.attributes.add('width', round(this.pattern.width) + 'mm')
this.attributes.add('height', round(this.pattern.height) + 'mm')
2018-08-16 13:59:11 +02:00
}
2022-09-19 18:04:47 +02:00
this.attributes.add('viewBox', `0 0 ${round(this.pattern.width)} ${round(this.pattern.height)}`)
2022-09-18 15:11:10 +02:00
this.head = this.__renderHead()
this.tail = this.__renderTail()
2019-08-03 15:03:33 +02:00
this.svg = ''
this.layout = {} // Reset layout
2022-09-19 18:04:47 +02:00
this.activeStackIndex = 0
for (let stackId in this.pattern.stacks) {
this.activeStack = stackId
2022-09-19 23:35:52 +02:00
this.idPrefix = this.pattern.settings[this.activeStackIndex]?.idPrefix || 'fs-'
2022-09-19 18:04:47 +02:00
const stack = this.pattern.stacks[stackId]
2022-09-18 17:01:19 +02:00
if (!stack.hidden) {
const stackSvg = this.__renderStack(stack)
this.layout[stackId] = {
svg: stackSvg,
transform: stack.attributes.getAsArray('transform'),
2019-08-03 15:03:33 +02:00
}
2022-09-18 17:01:19 +02:00
this.svg += this.__openGroup(`${this.idPrefix}stack-${stackId}`, stack.attributes)
this.svg += stackSvg
2022-09-18 15:11:10 +02:00
this.svg += this.__closeGroup()
2018-07-14 16:04:39 +00:00
}
2022-09-19 18:04:47 +02:00
this.activeStackIndex++
}
2022-09-18 15:11:10 +02:00
this.svg = this.prefix + this.__renderSvgTag() + this.head + this.svg + this.tail
this.__runHooks('postRender')
2019-08-03 15:03:33 +02:00
return this.svg
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
//////////////////////////////////////////////
// PRIVATE METHODS //
//////////////////////////////////////////////
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup to close a group
*
* @private
* @return {string} svg - The SVG markup to open a group
*/
Svg.prototype.__closeGroup = function () {
this.__outdent()
return `${this.__nl()}</g>${this.__nl()}<!-- end of group #${this.openGroups.pop()} -->`
2019-08-03 15:03:33 +02:00
}
2022-09-18 15:11:10 +02:00
/**
* Escapes text for SVG output
*
* @private
* @param {string} text - The text to escape
* @return {string} escaped - The escaped text
*/
Svg.prototype.__escapeText = function (text) {
return text.replace(/"/g, '&#8220;')
}
2022-09-18 15:11:10 +02:00
/**
* Returs an unused ID
*
* @private
* @return {numer} id - The next free ID
*/
Svg.prototype.__getId = function () {
this.freeId += 1
return '' + this.freeId
2019-08-03 15:03:33 +02:00
}
2022-09-18 15:11:10 +02:00
/**
* Increases indentation by 1
*
* @private
* @return {Svg} this - The Svg instance
*/
Svg.prototype.__indent = function () {
this.tabs += 1
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
return this
2019-08-03 15:03:33 +02:00
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Runs the insertText lifecycle hook(s)
*
* @private
* @param {string} text - The text to insert
* @return {Svg} this - The Svg instance
*/
Svg.prototype.__insertText = function (text) {
if (this.hooks.insertText.length > 0) {
for (let hook of this.hooks.insertText)
text = hook.method(
2022-10-23 19:49:58 +02:00
this.pattern.settings[this.pattern.activeSet].locale || 'en',
text,
hook.data
)
2022-09-18 15:11:10 +02:00
}
return text
2019-08-03 15:03:33 +02:00
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for a linebreak + indentation
*
* @private
* @return {string} svg - The Svg markup for a linebreak + indentation
*/
Svg.prototype.__nl = function () {
return '\n' + this.__tab()
}
/**
* Decreases indentation by 1
*
* @private
* @return {Svg} this - The Svg instance
*/
Svg.prototype.__outdent = function () {
this.tabs -= 1
return this
}
/**
* Returns SVG markup to open a group
*
* @private
* @param {text} id - The group id
* @param {Attributes} attributes - Any other attributes for the group
* @return {string} svg - The SVG markup to open a group
*/
Svg.prototype.__openGroup = function (id, attributes = false) {
let svg = this.__nl() + this.__nl()
svg += `<!-- Start of group #${id} -->`
svg += this.__nl()
svg += `<g id="${id}"`
if (attributes) svg += ` ${attributes.render()}`
svg += '>'
this.__indent()
this.openGroups.push(id)
2018-07-23 20:14:32 +02:00
2019-08-03 15:03:33 +02:00
return svg
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for a circle
*
* @private
* @param {Point} point - The Point instance that holds the circle data
* @return {string} svg - The SVG markup for the circle
*/
Svg.prototype.__renderCircle = function (point) {
return `<circle cx="${round(point.x)}" cy="${round(point.y)}" r="${point.attributes.get(
'data-circle'
)}" ${point.attributes.renderIfPrefixIs('data-circle-')}></circle>`
}
/**
* Returns SVG markup for the defs block
*
* @private
* @return {string} svg - The SVG markup for the defs block
*/
Svg.prototype.__renderDefs = function () {
2019-08-03 15:03:33 +02:00
let svg = '<defs>'
2022-09-18 15:11:10 +02:00
this.__indent()
svg += this.__nl() + this.defs
this.__outdent()
svg += this.__nl() + '</defs>' + this.__nl()
2018-07-23 20:14:32 +02:00
2019-08-03 15:03:33 +02:00
return svg
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for the head section
*
* @private
* @return {string} svg - The SVG markup for the head section
*/
Svg.prototype.__renderHead = function () {
let svg = this.__renderStyle()
svg += this.__renderDefs()
svg += this.__openGroup(this.idPrefix + 'container')
2019-08-03 15:03:33 +02:00
return svg
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for a Path object
*
* @private
* @param {Path} part - The Path instance to render
* @return {string} svg - The SVG markup for the Path object
*/
Svg.prototype.__renderPath = function (path) {
if (!path.attributes.get('id')) path.attributes.add('id', this.idPrefix + this.__getId())
2019-08-03 15:03:33 +02:00
path.attributes.set('d', path.asPathstring())
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
return `${this.__nl()}<path ${path.attributes.render()} />${this.__renderPathText(path)}`
2019-08-03 15:03:33 +02:00
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for the text on a Path object
*
* @private
* @param {Path} path - The Path instance that holds the text render
* @return {string} svg - The SVG markup for the text on a Path object
*/
Svg.prototype.__renderPathText = function (path) {
2019-08-03 15:03:33 +02:00
let text = path.attributes.get('data-text')
if (!text) return ''
2022-09-18 15:11:10 +02:00
else this.text = this.__insertText(text)
2019-08-03 15:03:33 +02:00
let attributes = path.attributes.renderIfPrefixIs('data-text-')
2018-07-25 14:53:10 +00:00
// Sadly aligning text along a patch can't be done in CSS only
2019-08-03 15:03:33 +02:00
let offset = ''
let align = path.attributes.get('data-text-class')
if (align && align.indexOf('center') > -1) offset = ' startOffset="50%" '
else if (align && align.indexOf('right') > -1) offset = ' startOffset="100%" '
2022-09-18 15:11:10 +02:00
let svg = this.__nl() + '<text>'
this.__indent()
svg += `<textPath xlink:href="#${path.attributes.get(
'id'
2022-09-18 15:11:10 +02:00
)}" ${offset}><tspan ${attributes}>${this.__escapeText(this.text)}</tspan></textPath>`
this.__outdent()
svg += this.__nl() + '</text>'
2019-08-03 15:03:33 +02:00
return svg
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for a Part object
*
* @private
* @param {Part} part - The Part instance to render
* @return {string} svg - The SVG markup for the Part object
*/
2022-09-19 18:04:47 +02:00
Svg.prototype.__renderPart = function (part) {
let svg = this.__openGroup(
`${this.idPrefix}stack-${this.activeStack}-part-${part.name}`,
part.attributes
)
2022-09-18 15:11:10 +02:00
for (let key in part.paths) {
let path = part.paths[key]
2022-09-18 17:01:19 +02:00
if (!path.hidden) svg += this.__renderPath(path)
}
2022-09-18 15:11:10 +02:00
for (let key in part.points) {
if (part.points[key].attributes.get('data-text')) {
svg += this.__renderText(part.points[key])
2018-08-14 12:56:25 +02:00
}
2022-09-18 15:11:10 +02:00
if (part.points[key].attributes.get('data-circle')) {
svg += this.__renderCircle(part.points[key])
}
}
for (let key in part.snippets) {
let snippet = part.snippets[key]
svg += this.__renderSnippet(snippet, part)
2018-08-14 12:56:25 +02:00
}
2022-09-18 17:01:19 +02:00
svg += this.__closeGroup()
2018-07-23 20:14:32 +02:00
2019-08-03 15:03:33 +02:00
return svg
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for a snippet
*
* @private
* @param {Snippet} snippet - The Snippet instance to render
* @return {string} svg - The SVG markup for the snippet
*/
Svg.prototype.__renderSnippet = function (snippet) {
let x = round(snippet.anchor.x)
let y = round(snippet.anchor.y)
let scale = snippet.attributes.get('data-scale') || 1
scale = scale * (this.pattern.settings.scale || 1)
if (scale) {
2019-08-03 15:03:33 +02:00
snippet.attributes.add('transform', `translate(${x}, ${y})`)
snippet.attributes.add('transform', `scale(${scale})`)
snippet.attributes.add('transform', `translate(${x * -1}, ${y * -1})`)
}
2019-08-03 15:03:33 +02:00
let rotate = snippet.attributes.get('data-rotate')
if (rotate) {
2019-08-03 15:03:33 +02:00
snippet.attributes.add('transform', `rotate(${rotate}, ${x}, ${y})`)
}
2022-09-18 15:11:10 +02:00
let svg = this.__nl()
2019-08-03 15:03:33 +02:00
svg += `<use x="${x}" y="${y}" `
svg += `xlink:href="#${snippet.def}" ${snippet.attributes.render()}>`
svg += '</use>'
2018-07-23 20:14:32 +02:00
2019-08-03 15:03:33 +02:00
return svg
}
2018-07-23 20:14:32 +02:00
2022-09-18 17:01:19 +02:00
/**
* Returns SVG markup for a Stack object
*
* @private
* @param {Stack} stack - The Stack instance to render
* @return {string} svg - The SVG markup for the Stack object
*/
Svg.prototype.__renderStack = function (stack) {
let svg = ''
for (const part of stack.parts) svg += this.__renderPart(part)
return svg
}
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for the style block
*
* @private
* @return {string} svg - The SVG markup for the style block
*/
Svg.prototype.__renderStyle = function () {
let svg = '<style type="text/css"> <![CDATA[ '
this.__indent()
svg += this.__nl() + this.style
this.__outdent()
svg += this.__nl() + ']]>' + this.__nl() + '</style>' + this.__nl()
2019-08-03 15:03:33 +02:00
return svg
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for the opening SVG tag
*
* @private
* @return {string} svg - The SVG markup for the SVG tag
*/
Svg.prototype.__renderSvgTag = function () {
let svg = '<svg'
this.__indent()
svg += this.__nl() + this.attributes.render()
this.__outdent()
svg += this.__nl() + '>' + this.__nl()
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
return svg
2019-08-03 15:03:33 +02:00
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for the closing section
*
* @private
* @return {string} svg - The SVG markup for the closing section
*/
Svg.prototype.__renderTail = function () {
let svg = ''
svg += this.__closeGroup()
svg += this.__nl() + '</svg>'
return svg
2019-08-03 15:03:33 +02:00
}
2018-07-23 20:14:32 +02:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for text
*
* @private
* @param {Point} point - The Point instance that holds the text render
* @return {string} svg - The SVG markup for text
*/
Svg.prototype.__renderText = function (point) {
let text = point.attributes.getAsArray('data-text')
if (text !== false) {
let joint = ''
for (let string of text) {
this.text = this.__insertText(string)
joint += this.text + ' '
}
this.text = this.__insertText(joint)
2018-07-23 20:14:32 +02:00
}
2022-09-18 15:11:10 +02:00
point.attributes.set('data-text-x', round(point.x))
point.attributes.set('data-text-y', round(point.y))
let lineHeight =
point.attributes.get('data-text-lineheight') || 6 * (this.pattern.settings.scale || 1)
point.attributes.remove('data-text-lineheight')
let svg = `${this.__nl()}<text ${point.attributes.renderIfPrefixIs('data-text-')}>`
this.__indent()
// Multi-line text?
if (this.text.indexOf('\n') !== -1) {
let lines = this.text.split('\n')
svg += `<tspan>${lines.shift()}</tspan>`
for (let line of lines) {
svg += `<tspan x="${round(point.x)}" dy="${lineHeight}">${line}</tspan>`
}
} else {
svg += `<tspan>${this.__escapeText(this.text)}</tspan>`
}
this.__outdent()
svg += this.__nl() + '</text>'
2018-07-14 16:04:39 +00:00
2022-09-18 15:11:10 +02:00
return svg
2019-08-03 15:03:33 +02:00
}
2018-07-14 16:04:39 +00:00
2022-09-18 15:11:10 +02:00
/**
* Runs SVG lifecycle hooks
*
* @private
* @param {string} hookName - The lifecycle hook to run
* @param {mixed} data - Any data to pass to the hook method
* @return {string} svg - The SVG markup for the indentation
*/
Svg.prototype.__runHooks = function (hookName, data = false) {
if (data === false) data = this
let hooks = this.hooks[hookName]
if (hooks.length > 0) {
for (let hook of hooks) {
hook.method(data, hook.data)
}
}
2019-08-03 15:03:33 +02:00
}
2018-07-14 16:04:39 +00:00
2022-09-18 15:11:10 +02:00
/**
* Returns SVG markup for indentation
*
* @private
* @return {string} svg - The SVG markup for the indentation
*/
Svg.prototype.__tab = function () {
let space = ''
for (let i = 0; i < this.tabs; i++) {
space += ' '
}
2022-09-18 15:11:10 +02:00
return space
2019-08-03 15:03:33 +02:00
}