1
0
Fork 0
freesewing/sites/shared/components/workbench/draft/path/index.js

475 lines
15 KiB
JavaScript
Raw Normal View History

import React, { useState } from 'react'
2022-01-25 11:22:09 +01:00
import TextOnPath from '../text-on-path'
import { getProps } from '../utils'
import { round } from 'shared/utils'
2022-01-25 11:22:09 +01:00
2022-06-18 13:15:15 +02:00
export const pointCoords = point => point
? `[ ${round(point.x, 2)}, ${round(point.y, 2)} ]`
: null
2022-06-18 13:15:15 +02:00
export const Tr = ({ children }) => <tr className="border border-base-300">{children}</tr>
export const KeyTd = ({ children }) => <td className="p-3 text-right">{children}:</td>
export const ValTd = ({ children }) => <td className="p-3">{children}</td>
2022-06-18 17:02:19 +02:00
export const TextAlongPath = ({id, size, fill="var(--pattern-note)", txt}) => (
<text>
<textPath xlinkHref={`#${id}`} startOffset="50%">
<tspan
style={{ textAnchor: 'middle', fontSize: size }}
className="fill-neutral-content fill-opacity-50"
dy={size * -0.4}
>
{txt}
</tspan>
</textPath>
</text>
)
2022-06-18 17:02:19 +02:00
export const PointCircle = ({ point, size, className="stroke-neutral-content" }) => (
<circle
cx={point.x} cy={point.y} r={size/50}
className={className}
fill="none" strokeWidth={size/150} strokeOpacity="0.5"
/>
)
2022-06-18 12:49:49 +02:00
const CpCircle = ({ point, className="fill-lining no-stroke" }) => (
<circle
cx={point.x} cy={point.y} r={1.5}
className={className}
/>
)
const pathDimensions = (from, to, cp1=false, cp2=false, path=false) => {
const topLeft = {
x: (to.x < from.x) ? to.x : from.x,
y: (to.y < from.y) ? to.y : from.y,
}
const bottomRight = {
x: (to.x > from.x) ? to.x : from.x,
y: (to.y > from.y) ? to.y : from.y,
}
let bbox = false
// Deal with curves
if (cp1 && cp2) {
if (cp1.x < topLeft.x) topLeft.x = cp1.x
if (cp2.x < topLeft.x) topLeft.x = cp2.x
if (cp1.x > bottomRight.x) bottomRight.x = cp1.x
if (cp2.x > bottomRight.x) bottomRight.x = cp2.x
if (cp1.y < topLeft.y) topLeft.y = cp1.y
if (cp2.y < topLeft.y) topLeft.y = cp2.y
if (cp1.y > bottomRight.y) bottomRight.y = cp1.y
if (cp2.y > bottomRight.y) bottomRight.y = cp2.y
// This undocumented core methods returns the curve's bounding box
bbox = path.boundary()
}
const w = bottomRight.x - topLeft.x
const h = bottomRight.y - topLeft.y
const size = w > h ? w : h
return {
topLeft,
bottomRight,
w,
h,
size,
bbox
}
}
2022-06-18 17:02:19 +02:00
export const Defs = () => (
<defs>
<marker orient="auto" refY="0.0" refX="0.0" id="arrowTo" style={{overflow: 'visible'}}>
<path className="fill-neutral-content" d="M 0,0 L -12,-4 C -10,-2 -10,2 -12, 4 z" fillOpacity="0.5"></path>
</marker>
<marker orient="auto" refY="0.0" refX="0.0" id="arrowFrom" style={{overflow: 'visible'}}>
<path className="fill-neutral-content" d="M 0,0 L 12,-4 C 10,-2 10,2 12, 4 z" fillOpacity="0.5"></path>
</marker>
</defs>
)
2022-06-18 17:02:19 +02:00
export const svgProps = {
xmlns: "http://www.w3.org/2000/svg",
xmlnsSvg: "http://www.w3.org/2000/svg",
xmlnsXlink: "http://www.w3.org/1999/xlink",
style: { maxHeight: 'inherit', strokeLinecap: 'round', strokeLinejoin: 'round' },
}
const Line = (props) => {
const ops = props.path.ops
const from = ops[0].to
const to = ops[1].to
const { topLeft, bottomRight, w, h, size } = pathDimensions(from, to)
const id = `${props.partName}_${props.pathName}_${props.i}`
const xyProps = {
className: "stroke-neutral-content",
strokeOpacity: "0.5",
fill: "none",
strokeWidth: (size/300),
}
return (
<svg {...svgProps}
viewBox={`${topLeft.x - (size/10)} ${topLeft.y - (size/10)} ${w + (size/5)} ${h + (size/5)}`}
>
<path id={`${id}_x`} {...xyProps}
d={`M ${topLeft.x},${bottomRight.y} L ${bottomRight.x},${bottomRight.y}`}
/>
<TextAlongPath id={`${id}_x`} size={size/18} txt={round(bottomRight.x - topLeft.x, 2)+'mm'} />
<path id={`${id}_y`} {...xyProps}
d={`M ${topLeft.x},${bottomRight.y} L ${topLeft.x},${topLeft.y}`}
/>
<TextAlongPath id={`${id}_y`} size={size/18} txt={round(bottomRight.y - topLeft.y, 2)+'mm'} />
<path
id={id}
d={`M ${from.x},${from.y} L ${to.x},${to.y}`}
className="stroke-neutral-content"
strokeLinecap="round"
strokeWidth={(size/100)}
/>
<TextAlongPath id={id} size={size/18} txt={round(props.path.length(), 2)+'mm'} />
<PointCircle point={from} size={size} />
<PointCircle point={to} size={size} />
</svg>
)
}
const Curve = (props) => {
const ops = props.path.ops
const from = ops[0].to
const { to, cp1, cp2 } = ops[1]
const { topLeft, bottomRight, w, h, size, bbox } = pathDimensions(from, to, cp1, cp2, props.path)
const id = `${props.partName}_${props.pathName}_${props.i}`
const cpProps = {
className: "stroke-success",
strokeOpacity: "0.85",
fill: "none",
strokeWidth: (size/300),
}
const xyProps = {
...cpProps,
strokeOpacity: "0.5",
className: "stroke-neutral-content",
markerEnd: "url(#arrowTo)",
markerStart: "url(#arrowFrom)",
}
return (
<svg {...svgProps}
viewBox={`${topLeft.x - (size/10)} ${topLeft.y - (size/10)} ${w + (size/5)} ${h + (size/5)}`}
>
<Defs />
<path id={`${id}_x`} {...xyProps}
d={`M ${bbox.topLeft.x},${bbox.bottomRight.y} L ${bbox.bottomRight.x},${bbox.bottomRight.y}`}
/>
<TextAlongPath id={`${id}_x`} size={size/18} txt={round(bbox.bottomRight.x - bbox.topLeft.x, 2)+'mm'} />
<path id={`${id}_y`} {...xyProps}
d={`M ${bbox.topLeft.x},${bbox.bottomRight.y} L ${bbox.topLeft.x},${bbox.topLeft.y}`}
/>
<TextAlongPath id={`${id}_y`} size={size/18} txt={round(bbox.bottomRight.y - bbox.topLeft.y, 2)+'mm'} />
<path id={`${id}_cp1`} {...cpProps} d={`M ${from.x},${from.y} L ${cp1.x},${cp1.y}`} />
<PointCircle point={cp1} size={size} className="stroke-success" />
<path id={`${id}_cp2`} {...cpProps} d={`M ${to.x},${to.y} L ${cp2.x},${cp2.y}`} />
<PointCircle point={cp2} size={size} className="stroke-success"/>
<path
id={id}
d={`M ${from.x},${from.y} C ${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${to.x},${to.y}`}
className="stroke-neutral-content"
fill="none"
strokeLinecap="round"
strokeWidth={(size/100)}
/>
<TextAlongPath id={id} size={size/18} txt={round(props.path.length(), 2)+'mm'} />
<PointCircle point={from} size={size} />
<PointCircle point={to} size={size} />
</svg>
)
}
const MiniPath = props => {
const ops = props.path.ops
const bbox = props.path.boundary()
const id = `${props.partName}_${props.pathName}_mini}`
const w = bbox.bottomRight.x - bbox.topLeft.x
const h = bbox.bottomRight.y - bbox.topLeft.y
const size = w > h ? w : h
const xyProps = {
fill: "none",
strokeWidth: (size/300),
strokeOpacity: "0.5",
className: "stroke-neutral-content",
markerEnd: "url(#arrowTo)",
markerStart: "url(#arrowFrom)",
}
return (
<svg {...svgProps}
viewBox={`${bbox.topLeft.x - (size/10)} ${bbox.topLeft.y - (size/10)} ${w + (size/5)} ${h + (size/5)}`}
className="freesewing pattern z-50"
>
<Defs />
<path id={`${id}_x`} {...xyProps}
d={`M ${bbox.topLeft.x},${bbox.bottomRight.y} L ${bbox.bottomRight.x},${bbox.bottomRight.y}`}
/>
<TextAlongPath id={`${id}_x`} size={size/18} txt={round(bbox.bottomRight.x - bbox.topLeft.x, 2)+'mm'} />
<path id={`${id}_y`} {...xyProps}
d={`M ${bbox.topLeft.x},${bbox.bottomRight.y} L ${bbox.topLeft.x},${bbox.topLeft.y}`}
/>
<TextAlongPath id={`${id}_y`} size={size/18} txt={round(bbox.bottomRight.y - bbox.topLeft.y, 2)+'mm'} />
<path
id={id}
d={props.path.asPathstring()}
className="stroke-neutral-content"
fill="none"
strokeLinecap="round"
strokeWidth={(size/100)}
/>
<TextAlongPath id={id} size={size/18} txt={round(props.path.length(), 2)+'mm'} />
<XrayPath {...props} />)
</svg>
)
}
const lineInfo = (props) => (
<div className="p-4 border bg-neutral bg-opacity-60 shadow rounded-lg">
<h5 className="text-neutral-content text-center pb-4">Line info</h5>
<div className="flex flex-row flex-wrap">
<table className="border-collapse h-fit">
<tbody>
<Tr>
<KeyTd>From</KeyTd>
2022-06-18 13:15:15 +02:00
<ValTd>{pointCoords(props.path.ops[0].to)}</ValTd>
</Tr>
<Tr>
<KeyTd>To</KeyTd>
2022-06-18 13:15:15 +02:00
<ValTd>{pointCoords(props.path.ops[1].to)}</ValTd>
</Tr>
<Tr>
<KeyTd>Length</KeyTd>
<ValTd>{round(props.path.length(), 2)}mm</ValTd>
</Tr>
<Tr>
<KeyTd>Part</KeyTd>
<ValTd>{props.partName}</ValTd>
</Tr>
<Tr>
<KeyTd>Draw Op</KeyTd>
<ValTd>{props.i}/{props.ops.length}</ValTd>
</Tr>
</tbody>
</table>
<div className="max-w-md" style={{ maxHeight: '80vh' }}>
<Line {...props} />
</div>
</div>
</div>
)
const XrayLine = props => (
<>
2022-01-29 10:50:02 +01:00
<path
d={props.path.asPathstring()}
{...getProps(props.path)}
className="opacity-0 stroke-4xl stroke-note hover:opacity-25 hover:cursor-pointer"
onClick={(evt) => { evt.stopPropagation(); props.showInfo(lineInfo(props)) }}
2022-01-29 10:50:02 +01:00
/>
</>
)
const curveInfo = (props) => (
<div className="p-4 border bg-neutral bg-opacity-40 shadow rounded-lg">
<h5 className="text-neutral-content text-center pb-4">Curve info</h5>
<div className="flex flex-row flex-wrap">
<table className="border-collapse h-fit">
<tbody>
<Tr>
<KeyTd>From</KeyTd>
2022-06-18 13:15:15 +02:00
<ValTd>{pointCoords(props.path.ops[0].to)}</ValTd>
</Tr>
<Tr>
<KeyTd>Cp1</KeyTd>
2022-06-18 13:15:15 +02:00
<ValTd>{pointCoords(props.path.ops[1].cp1)}</ValTd>
</Tr>
<Tr>
<KeyTd>Cp2</KeyTd>
2022-06-18 13:15:15 +02:00
<ValTd>{pointCoords(props.path.ops[1].cp2)}</ValTd>
</Tr>
<Tr>
<KeyTd>To</KeyTd>
2022-06-18 13:15:15 +02:00
<ValTd>{pointCoords(props.path.ops[1].to)}</ValTd>
</Tr>
<Tr>
<KeyTd>Length</KeyTd>
<ValTd>{round(props.path.length(), 2)}mm</ValTd>
</Tr>
<Tr>
<KeyTd>Part</KeyTd>
<ValTd>{props.partName}</ValTd>
</Tr>
<Tr>
<KeyTd>Draw Op</KeyTd>
<ValTd>{props.i}/{props.ops.length}</ValTd>
</Tr>
</tbody>
</table>
<div className="max-w-md" style={{ maxHeight: '80vh' }}>
<Curve {...props} />
</div>
</div>
</div>
2022-01-29 10:50:02 +01:00
)
2022-06-18 13:15:15 +02:00
export const Attributes = ({ list }) => list
? (
<ul>
{Object.keys(list).map(key => (
<li><strong>{key}</strong>: {list[key]}</li>
))}
</ul>
) : null
const pathInfo = (props) => {
const bbox = props.path.boundary()
return (
<div className="p-4 border bg-neutral bg-opacity-40 shadow rounded-lg">
<h5 className="text-neutral-content text-center pb-4">Path info</h5>
<div className="flex flex-row flex-wrap overflow-scroll" style={{ maxHeight: '80vh' }}>
<table className="border-collapse h-fit">
<tbody>
<Tr>
<KeyTd>Name</KeyTd>
<ValTd>{bbox.name}</ValTd>
</Tr>
<Tr>
<KeyTd>Length</KeyTd>
<ValTd>{round(props.path.length(), 2)}mm</ValTd>
</Tr>
<Tr>
<KeyTd>Width</KeyTd>
<ValTd>{round(bbox.bottomRight.x - bbox.topLeft.x, 2)}mm</ValTd>
</Tr>
<Tr>
<KeyTd>Height</KeyTd>
<ValTd>{round(bbox.bottomRight.y - bbox.topLeft.y, 2)}mm</ValTd>
</Tr>
<Tr>
<KeyTd>Top Left</KeyTd>
2022-06-18 13:15:15 +02:00
<ValTd>{pointCoords(bbox.topLeft)}</ValTd>
</Tr>
<Tr>
<KeyTd>Bottom Right</KeyTd>
2022-06-18 13:15:15 +02:00
<ValTd>{pointCoords(bbox.bottomRight)}</ValTd>
</Tr>
<Tr>
<KeyTd>Part</KeyTd>
<ValTd>{props.partName}</ValTd>
</Tr>
<Tr>
<KeyTd>Attributes</KeyTd>
<ValTd><Attributes list={bbox.attributes.list} /></ValTd>
</Tr>
</tbody>
</table>
<table className="border-collapse h-fit">
<tbody>
{props.path.ops.map((op, i) => (
<Tr key={i}>
<KeyTd>{i}</KeyTd>
<ValTd><PathOp op={op}/></ValTd>
</Tr>
))}
</tbody>
</table>
<div className="max-w-md">
<MiniPath {...props} />
</div>
</div>
</div>
)
}
const PathOp = ({ op }) => {
2022-06-18 13:15:15 +02:00
if (op.type === 'move') return <span><strong>Move</strong> to {pointCoords(op.to)}</span>
else if (op.type === 'line') return <span><strong>Line</strong> to {pointCoords(op.to)}</span>
else if (op.type === 'curve') return (
<span>
2022-06-18 13:15:15 +02:00
<strong>Curve</strong> to {pointCoords(op.to)}
<br />
2022-06-18 13:15:15 +02:00
Cp1: {pointCoords(op.cp1)}
<br />
2022-06-18 13:15:15 +02:00
Cp2: {pointCoords(op.cp2)}
</span>
)
else if (op.type === 'noop') return <strong>NOOP</strong>
else if (op.type === 'close') return <strong>Close</strong>
else return <strong>FIXME: unknown path operation type: {op.type}</strong>
}
2022-06-18 12:49:49 +02:00
const XrayCurve = props => {
const from = props.path.ops[0].to
const { cp1, cp2, to } = props.path.ops[1]
return (
<>
<path
d={props.path.asPathstring()}
{...getProps(props.path)}
className="opacity-0 stroke-4xl stroke-lining hover:opacity-25 hover:cursor-pointer"
onClick={(evt) => { evt.stopPropagation(); props.showInfo(curveInfo(props)) }}
/>
<path d={`M ${from.x},${from.y} L ${cp1.x},${cp1.y}`} className="lining dotted"/>
<path d={`M ${to.x},${to.y} L ${cp2.x},${cp2.y}`} className="lining dotted"/>
<CpCircle point={cp1} />
<CpCircle point={cp2} />
</>
)
}
const XrayPath = props => {
const ops = props.path.divide()
return (
<g>
<path
d={props.path.asPathstring()}
{...getProps(props.path)}
className="opacity-0 stroke-7xl stroke-contrast hover:opacity-25 hover:cursor-pointer"
onClick={(evt) => { evt.preventDefault(); props.showInfo(pathInfo(props)) }}
/>
2022-06-18 12:49:49 +02:00
{ops.length > 0
? ops.map((op,i) => (op.ops[1].type === 'curve')
? <XrayCurve {...props} path={op} ops={ops} i={i} pathName={`${props.pathName}_test`} />
: <XrayLine {...props} path={op} ops={ops} i={i} pathName={`${props.pathName}_test`} />
) : null
}
</g>
)
}
2022-01-29 10:50:02 +01:00
2022-01-28 19:55:32 +01:00
const Path = props => {
2022-01-29 10:50:02 +01:00
const { path, partName, pathName } = props
2022-01-28 19:55:32 +01:00
if (!path.render) return null
2022-01-25 11:22:09 +01:00
const output = []
2022-01-29 10:50:02 +01:00
const pathId = 'path-' + partName + '-' + pathName
let d = ''
try { d = path.asPathstring() }
catch (err) {
// Bail out
console.log(`Failed to generate pathstring for path ${pathId} in part ${partName}`, err)
return null
}
output.push(<path id={pathId} key={pathId} d={d} {...getProps(path)} />)
2022-01-28 19:55:32 +01:00
if (path.attributes.get('data-text'))
output.push(<TextOnPath key={'text-on-path-' + name} pathId={pathId} {...props} />)
if (props.gist._state?.xray?.enabled) output.push(<XrayPath {...props} key={'xpath'+pathId} />)
2022-01-25 11:22:09 +01:00
return output
}
export default Path