1
0
Fork 0

[breaking]: FreeSewing v4 (#7297)

Refer to the CHANGELOG for all info.

---------

Co-authored-by: Wouter van Wageningen <wouter.vdub@yahoo.com>
Co-authored-by: Josh Munic <jpmunic@gmail.com>
Co-authored-by: Jonathan Haas <haasjona@gmail.com>
This commit is contained in:
Joost De Cock 2025-04-01 16:15:20 +02:00 committed by GitHub
parent d22fbe78d9
commit 51dc1d9732
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6626 changed files with 142053 additions and 150606 deletions

View file

@ -0,0 +1,14 @@
import React from 'react'
export const Circle = ({ point }) =>
point.attributes.list['data-circle'].map((r, i) => {
const circleProps = point.attributes.circleProps
const extraProps = {}
for (const prop in circleProps) {
const val = point.attributes.list[`data-circle-${prop === 'className' ? 'class' : prop}`]
if (val.length >= i) extraProps[prop] = val[i]
else extraProps[prop] = val.join(' ')
}
return <circle key={r} cx={point.x} cy={point.y} r={r} {...extraProps} />
})

View file

@ -0,0 +1,97 @@
// eslint-disable-next-line no-unused-vars
import React from 'react'
import sanitize from 'html-react-parser'
const style = { fill: 'none', stroke: 'currentColor' }
const StackDefs = ({ stacks }) =>
Object.keys(stacks).map((stackName) => {
const part = stacks[stackName].parts[0]
let anchor = { x: 0, y: 0 }
if (typeof part.points.gridAnchor !== 'undefined') anchor = part.points.gridAnchor
else if (typeof part.points.anchor !== 'undefined') anchor = part.points.anchor
if (isNaN(anchor.x)) anchor.x = 0
if (isNaN(anchor.y)) anchor.y = 0
return (
<pattern
id={`grid-${stackName}`}
key={`grid-${stackName}`}
xlinkHref="#grid"
x={anchor.x}
y={anchor.y}
/>
)
})
const MetricPaperlessDefs = ({ stacks }) => (
<>
<pattern id="grid" height="100" width="100" patternUnits="userSpaceOnUse" key="grid">
<path style={style} className="gridline lg metric" d="M 0 0 L 0 100 L 100 100" />
<path style={style} className="gridline metric" d="M 50 0 L 50 100 M 0 50 L 100 50" />
<path
style={style}
className="gridline sm metric"
d="M 10 0 L 10 100 M 20 0 L 20 100 M 30 0 L 30 100 M 40 0 L 40 100 M 60 0 L 60 100 M 70 0 L 70 100 M 80 0 L 80 100 M 90 0 L 90 100"
/>
<path
style={style}
className="gridline sm metric"
d="M 0 10 L 100 10 M 0 20 L 100 20 M 0 30 L 100 30 M 0 40 L 100 40 M 0 60 L 100 60 M 0 70 L 100 70 M 0 80 L 100 80 M 0 90 L 100 90"
/>
<path
style={style}
className="gridline xs metric"
d="M 5 0 L 5 100 M 15 0 L 15 100 M 25 0 L 25 100 M 35 0 L 35 100 M 45 0 L 45 100 M 55 0 L 55 100 M 65 0 L 65 100 M 75 0 L 75 100 M 85 0 L 85 100 M 95 0 L 95 100"
/>
<path
style={style}
className="gridline xs metric"
d="M 0 5 L 100 5 M 0 15 L 100 15 M 0 25 L 100 25 M 0 35 L 100 35 M 0 45 L 100 45 M 0 55 L 100 55 M 0 65 L 100 65 M 0 75 L 100 75 M 0 85 L 100 85 M 0 95 L 100 95"
/>
</pattern>
<StackDefs stacks={stacks} />
</>
)
const ImperialPaperlessDefs = ({ stacks }) => (
<>
<pattern id="grid" height="25.4" width="25.4" patternUnits="userSpaceOnUse" key="grid">
<path style={style} className="gridline lg imperial" d="M 0 0 L 0 25.4 L 25.4 25.4" />
<path
style={style}
className="gridline lg imperial"
d="M 12.7 0 L 12.7 25.4 M 0 12.7 L 25.4 12.7"
/>
<path
style={style}
className="gridline sm imperial"
d="M 3.175 0 L 3.175 25.4 M 6.32 0 L 6.35 25.4 M 9.525 0 L 9.525 25.4 M 15.875 0 L 15.875 25.4 M 19.05 0 L 19.05 25.4 M 22.225 0 L 22.225 25.4"
/>
<path
style={style}
className="gridline sm imperial"
d="M 0 3.175 L 25.4 3.175 M 0 6.32 L 25.4 6.35 M 0 9.525 L 25.4 9.525 M 0 15.875 L 25.4 15.875 M 0 19.05 L 25.4 19.05 M 0 22.225 L 25.4 22.225"
/>
</pattern>
<StackDefs stacks={stacks} />
</>
)
const PaperlessDefs = ({ units = 'metric', stacks }) =>
units === 'imperial' ? (
<ImperialPaperlessDefs stacks={stacks} />
) : (
<MetricPaperlessDefs stacks={stacks} />
)
export const Defs = (props) =>
props.svg ? (
<defs>
{props.svg.defs.list ? sanitize(Object.values(props.svg.defs.list).join('')) : null}
{props.settings[0].paperless ? (
<PaperlessDefs units={props.settings[0].units} stacks={props.stacks} />
) : null}
</defs>
) : null

View file

@ -0,0 +1,14 @@
// __SDEFILE__ - This file is a dependency for the stand-alone environment
// eslint-disable-next-line no-unused-vars
import React from 'react'
export const Grid = ({ stack, stackName }) => (
<rect
x={stack.topLeft.x}
y={stack.topLeft.y}
width={stack.width}
height={stack.height}
className="grid"
fill={'url(#grid-' + stackName + ')'}
/>
)

View file

@ -0,0 +1,11 @@
// __SDEFILE__ - This file is a dependency for the stand-alone environment
// eslint-disable-next-line no-unused-vars
import React, { forwardRef } from 'react'
export const Group = forwardRef((props, ref) => (
<g {...props} ref={ref}>
{props.children}
</g>
))
Group.displayName = 'Group'

View file

@ -0,0 +1,104 @@
// Dependencies
import { cloudflareImageUrl } from '@freesewing/utils'
// Components
import React, { forwardRef } from 'react'
import { Svg as DefaultSvg } from './svg.mjs'
import { Defs as DefaultDefs } from './defs.mjs'
import { Group as DefaultGroup } from './group.mjs'
import { Stack as DefaultStack } from './stack.mjs'
import { Part as DefaultPart } from './part.mjs'
import { Point as DefaultPoint } from './point.mjs'
import { Snippet as DefaultSnippet } from './snippet.mjs'
import { Path as DefaultPath } from './path.mjs'
import { Grid as DefaultGrid } from './grid.mjs'
import { Text as DefaultText, TextOnPath as DefaultTextOnPath } from './text.mjs'
import { Circle as DefaultCircle } from './circle.mjs'
import { getId, getProps, withinPartBounds, translateStrings } from './utils.mjs'
import { Link as WebLink } from '@freesewing/react/components/Link'
/*
* Allow people to override these components
*/
const defaultComponents = {
Svg: DefaultSvg,
Defs: DefaultDefs,
Group: DefaultGroup,
Stack: DefaultStack,
Part: DefaultPart,
Point: DefaultPoint,
Path: DefaultPath,
Snippet: DefaultSnippet,
Grid: DefaultGrid,
Text: DefaultText,
TextOnPath: DefaultTextOnPath,
Circle: DefaultCircle,
}
/*
* The pattern component
* FIXME: document props
*/
const Pattern = forwardRef((props, ref) => {
if (!props.renderProps) return null
// Destructure props
const {
renderProps = false,
strings = {},
children = false,
className = 'freesewing pattern',
components = {},
} = props
// Merge default and swizzled components
const mergedComponents = {
...defaultComponents,
...components,
}
const { Svg, Defs, Stack, Group } = mergedComponents
const optionalProps = {}
if (className) optionalProps.className = className
return (
<Svg
viewBox={`0 0 ${renderProps.width} ${renderProps.height}`}
embed={renderProps.settings.embed}
{...renderProps}
{...optionalProps}
ref={ref}
>
<Defs {...renderProps} />
<style>{`:root { --pattern-scale: ${renderProps.settings.scale || 1}} ${
renderProps.svg.style
}`}</style>
<Group>
{children
? children
: Object.keys(renderProps.stacks).map((stackName) => (
<Stack
key={stackName}
stackName={stackName}
stack={renderProps.stacks[stackName]}
settings={renderProps.settings}
components={mergedComponents}
strings={strings}
/>
))}
</Group>
</Svg>
)
})
export {
// utils
getId,
getProps,
withinPartBounds,
translateStrings,
// default components
defaultComponents,
// The Pattern component itself
Pattern,
}

View file

@ -0,0 +1,52 @@
// eslint-disable-next-line no-unused-vars
import React, { forwardRef } from 'react'
import { getId, getProps } from './utils.mjs'
export const PartInner = forwardRef(
({ stackName, partName, part, settings, components, strings }, ref) => {
const { Group, Path, Point, Snippet } = components
return (
<Group ref={ref} id={getId({ settings, stackName, partName, name: 'inner' })}>
{Object.keys(part.paths).map((pathName) => (
<Path
key={pathName}
path={part.paths[pathName]}
topLeft={part.topLeft}
bottomRight={part.bottomRight}
units={settings[0].units}
{...{ stackName, partName, pathName, part, settings, components, strings }}
/>
))}
{Object.keys(part.points).map((pointName) => (
<Point
key={pointName}
point={part.points[pointName]}
topLeft={part.topLeft}
bottomRight={part.bottomRight}
{...{ stackName, partName, pointName, part, settings, components, strings }}
/>
))}
{Object.keys(part.snippets).map((snippetName) => (
<Snippet
key={snippetName}
snippet={part.snippets[snippetName]}
{...{ stackName, partName, snippetName, part, settings, components, strings }}
/>
))}
</Group>
)
}
)
PartInner.displayName = 'PartInner'
export const Part = ({ stackName, partName, part, settings, components, strings }) => {
const { Group } = components
return (
<Group {...getProps(part)} id={getId({ settings, stackName, partName })}>
<PartInner {...{ stackName, partName, part, settings, components, strings }} />
</Group>
)
}

View file

@ -0,0 +1,22 @@
// eslint-disable-next-line no-unused-vars
import React from 'react'
import { getId, getProps } from './utils.mjs'
export const Path = ({ stackName, pathName, path, partName, settings, components, strings }) => {
// Don't render hidden paths
if (path.hidden) return null
// Get potentially swizzled components
const { TextOnPath } = components
const pathId = getId({ settings, stackName, partName, pathName })
return (
<>
<path id={pathId} d={path.d} {...getProps(path)} />
{path.attributes.text && path.attributes.text.length > 0 ? (
<TextOnPath {...{ path, pathId, strings }} />
) : null}
</>
)
}

View file

@ -0,0 +1,23 @@
// eslint-disable-next-line no-unused-vars
import React from 'react'
import { withinPartBounds } from './utils.mjs'
export const Point = ({ stackName, partName, pointName, part, point, components, strings }) => {
/*
* Don't include points outside the part bounding box
* Unless the `data-render-always` attribute is set
*/
if (!withinPartBounds(point, part) && !point.attributes.list['data-render-always']) return null
// Get potentially swizzled components
const { Circle, Text } = components
return point.attributes ? (
<>
{point.attributes.text ? (
<Text {...{ point, pointName, partName, stackName, strings }} />
) : null}
{point.attributes.circle ? <Circle point={point} /> : null}
</>
) : null
}

View file

@ -0,0 +1,29 @@
// __SDEFILE__ - This file is a dependency for the stand-alone environment
// eslint-disable-next-line no-unused-vars
import React from 'react'
import { getProps } from './utils.mjs'
export const Snippet = ({ snippet, settings }) => {
if (!snippet?.anchor || !snippet.def) return null
if (!settings[0].complete && !snippet.attributes.list?.['data-force']?.[0]) return null
const snippetProps = {
xlinkHref: '#' + snippet.def,
x: snippet.anchor.x,
y: snippet.anchor.y,
}
const scale = snippet.attributes.list['data-scale']?.[0] || false
const rotate = snippet.attributes.list['data-rotate']?.[0] || false
if (scale || rotate) {
snippetProps.transform = ''
if (scale) {
snippetProps.transform += `translate(${snippetProps.x}, ${snippetProps.y}) `
snippetProps.transform += `scale(${scale}) `
snippetProps.transform += `translate(${snippetProps.x * -1}, ${snippetProps.y * -1}) `
}
if (rotate) {
snippetProps.transform += `rotate(${rotate}, ${snippetProps.x}, ${snippetProps.y}) `
}
}
return <use {...snippetProps} {...getProps(snippet)} color="currentColor" />
}

View file

@ -0,0 +1,15 @@
import React from 'react'
import { getProps } from './utils.mjs'
export const Stack = ({ stackName, stack, settings, components, strings }) => {
const { Group, Part, Grid } = components
return (
<Group {...getProps(stack)}>
{settings[0].paperless ? <Grid {...{ stack, stackName }} /> : null}
{[...stack.parts].map((part, key) => (
<Part {...{ settings, components, part, stackName, strings }} key={key} />
))}
</Group>
)
}

View file

@ -0,0 +1,44 @@
// eslint-disable-next-line no-unused-vars
import React from 'react'
import { forwardRef } from 'react'
export const Svg = forwardRef(
(
{
embed = true,
locale = 'en',
className = 'freesewing pattern',
style = {},
viewBox = false,
width,
height,
children,
},
ref
) => {
if (width < 1) width = 1000
if (height < 1) height = 1000
let attributes = {
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:svg': 'http://www.w3.org/2000/svg',
xmlnsXlink: 'http://www.w3.org/1999/xlink',
xmlLang: locale,
viewBox: viewBox || `0 0 ${width} ${height}`,
className,
style,
}
if (!embed) {
attributes.width = width + 'mm'
attributes.height = height + 'mm'
}
return (
<svg {...attributes} ref={ref}>
{children}
</svg>
)
}
)
Svg.displayName = 'Svg'

View file

@ -0,0 +1,55 @@
// eslint-disable-next-line no-unused-vars
import React from 'react'
import { translateStrings } from './utils.mjs'
export const TextSpans = ({ point, strings }) => {
const translated = translateStrings(point.attributes.list['data-text'], strings)
const text = []
if (translated.indexOf('\n') !== -1) {
// Handle muti-line text
let key = 0
let lines = translated.split('\n')
text.push(<tspan key={'tspan-' + key}>{lines.shift()}</tspan>)
for (let line of lines) {
key++
text.push(
<tspan
key={'tspan-' + key}
x={point.x}
dy={point.attributes.list['data-text-lineheight']?.[0] || 12}
>
{line.toString().replace(/&quot;/g, '"')}
</tspan>
)
}
} else text.push(<tspan key="tspan">{translated}</tspan>)
return text
}
export const Text = ({ point, strings }) => (
<text x={point.x} y={point.y} {...point.attributes.textProps}>
<TextSpans point={point} strings={strings} />
</text>
)
export const TextOnPath = ({ path, pathId, strings }) => {
const textPathProps = {
xlinkHref: '#' + pathId,
startOffset: '0%',
}
const translated = translateStrings(path.attributes.text, strings)
const align = path.attributes.list['data-text-class']
? path.attributes.list['data-text-class'].join(' ')
: false
if (align && align.indexOf('center') > -1) textPathProps.startOffset = '50%'
else if (align && align.indexOf('right') > -1) textPathProps.startOffset = '100%'
return (
<text>
<textPath {...textPathProps}>
<tspan {...path.attributes.textProps} dangerouslySetInnerHTML={{ __html: translated }} />
</textPath>
</text>
)
}

View file

@ -0,0 +1,82 @@
import React from 'react'
export const getProps = (obj) => {
/** I can't believe it but there seems to be no method on NPM todo this */
const cssKey = (key) => {
let chunks = key.split('-')
if (chunks.length > 1) {
key = chunks.shift()
for (let s of chunks) key += s.charAt(0).toUpperCase() + s.slice(1)
}
return key
}
const convert = (css) => {
let style = {}
let rules = css.split(';')
for (let rule of rules) {
let chunks = rule.split(':')
if (chunks.length === 2) style[cssKey(chunks[0].trim())] = chunks[1].trim()
}
return style
}
let rename = {
class: 'className',
'marker-start': 'markerStart',
'marker-end': 'markerEnd',
}
let props = {}
for (let key in obj.attributes.list) {
if (key === 'style') props[key] = convert(obj.attributes.list[key].join(' '))
if (Object.keys(rename).indexOf(key) !== -1)
props[rename[key]] = obj.attributes.list[key].join(' ')
else if (key !== 'style') props[key] = obj.attributes.list[key].join(' ')
}
return props
}
export const withinPartBounds = (point, part) =>
point.x >= part.topLeft.x &&
point.x <= part.bottomRight.x &&
point.y >= part.topLeft.y &&
point.y <= part.bottomRight.y
? true
: false
export const getId = ({
settings = {},
stackName = false,
partName = false,
pathName = false,
pointName = false,
snippetName = false,
name = false,
}) => {
let id = settings.idPrefix || ''
if (stackName) id += `${stackName}-`
if (partName) id += `${partName}-`
if (pathName) id += `${pathName}-`
if (pointName) id += `${pointName}-`
if (snippetName) id += `${snippetName}-`
if (name) id += name
return id
}
export const translateStrings = (list, translations = {}) => {
let translated = ''
if (!list) return translated
for (const string of list) {
if (Array.isArray(string)) translated += translateStrings(string, translations)
else if (string) {
if (translations[string]) {
translated += `${translations[string]}`.replace(/&quot;/g, '"') + ' '
} else translated += `${string}`
}
}
return translated
}