import TextOnPath from '../text-on-path' import { getProps } from '../utils' import { round, formatMm } from 'shared/utils' import RawSpan from 'shared/components/raw-span' export const pointCoords = point => point ? `[ ${round(point.x, 2)}, ${round(point.y, 2)} ]` : null 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> 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> ) 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" /> ) 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 } } 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> ) 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={formatMm(bottomRight.x - topLeft.x, props.gist.units, 'notags')} /> <path id={`${id}_y`} {...xyProps} d={`M ${topLeft.x},${bottomRight.y} L ${topLeft.x},${topLeft.y}`} /> <TextAlongPath id={`${id}_y`} size={size/18} txt={formatMm(bottomRight.y - topLeft.y, props.gist.units, 'notags')} /> <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={formatMm(props.path.length(), props.gist.units, 'notags')} /> <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, 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={formatMm(bbox.bottomRight.x - bbox.topLeft.x, props.gist.units, 'notags')} /> <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={formatMm(bbox.bottomRight.y - bbox.topLeft.y, props.gist.units, 'notags')} /> <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={formatMm(props.path.length(), props.gist.units, 'notags')} /> <PointCircle point={from} size={size} /> <PointCircle point={to} size={size} /> </svg> ) } const MiniPath = props => { 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={formatMm(bbox.bottomRight.x - bbox.topLeft.x, props.gist.units, 'notags')} /> <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={formatMm(bbox.bottomRight.y - bbox.topLeft.y, props.gist.units, 'notags')} /> <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={formatMm(props.path.length(), props.gist.units, 'notags')} /> <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> <ValTd>{pointCoords(props.path.ops[0].to)}</ValTd> </Tr> <Tr> <KeyTd>To</KeyTd> <ValTd>{pointCoords(props.path.ops[1].to)}</ValTd> </Tr> <Tr> <KeyTd>Length</KeyTd> <ValTd>{formatMm(props.path.length(), props.gist.units, 'notags')}</ValTd> </Tr> <Tr> <KeyTd>Width</KeyTd> <ValTd> <RawSpan html={formatMm(Math.abs(props.path.ops[0].to.dx(props.path.ops[1].to)), props.gist.units)} /> </ValTd> </Tr> <Tr> <KeyTd>Height</KeyTd> <ValTd> <RawSpan html={formatMm(Math.abs(props.path.ops[0].to.dy(props.path.ops[1].to)), props.gist.units)} /> </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 => ( <> <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)) }} /> </> ) 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> <ValTd>{pointCoords(props.path.ops[0].to)}</ValTd> </Tr> <Tr> <KeyTd>Cp1</KeyTd> <ValTd>{pointCoords(props.path.ops[1].cp1)}</ValTd> </Tr> <Tr> <KeyTd>Cp2</KeyTd> <ValTd>{pointCoords(props.path.ops[1].cp2)}</ValTd> </Tr> <Tr> <KeyTd>To</KeyTd> <ValTd>{pointCoords(props.path.ops[1].to)}</ValTd> </Tr> <Tr> <KeyTd>Length</KeyTd> <ValTd> <RawSpan html={formatMm(props.path.length(), props.gist.units)}/> </ValTd> </Tr> <Tr> <KeyTd>Width</KeyTd> <ValTd> <RawSpan html={formatMm(Math.abs(props.path.ops[0].to.dx(props.path.ops[1].to)), props.gist.units)}/> </ValTd> </Tr> <Tr> <KeyTd>Height</KeyTd> <ValTd> <RawSpan html={formatMm(Math.abs(props.path.ops[0].to.dy(props.path.ops[1].to)), props.gist.units)}/> </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> ) export const Attributes = ({ list }) => list ? ( <ul> {Object.keys(list).map(key => ( <li key={key}><strong>{key}</strong>: {list[key]}</li> ))} </ul> ) : null export 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' }}> <div> <table className="border-collapse h-fit"> <tbody> <Tr> <KeyTd>Name</KeyTd> <ValTd>{bbox.name}</ValTd> </Tr> <Tr> <KeyTd>Length</KeyTd> <ValTd> <RawSpan html={formatMm(props.path.length(), props.gist.units)} /> </ValTd> </Tr> <Tr> <KeyTd>Width</KeyTd> <ValTd> <RawSpan html={formatMm(Math.abs(bbox.bottomRight.x - bbox.topLeft.x), props.gist.units)} /> </ValTd> </Tr> <Tr> <KeyTd>Height</KeyTd> <ValTd> <RawSpan html={formatMm(Math.abs(bbox.bottomRight.y - bbox.topLeft.y), props.gist.units)} /> </ValTd> </Tr> <Tr> <KeyTd>Top Left</KeyTd> <ValTd>{pointCoords(bbox.topLeft)}</ValTd> </Tr> <Tr> <KeyTd>Bottom Right</KeyTd> <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> <div className="flex flex-row flex-wrap gap-2 mt-4"> <button className="btn btn-success" onClick={() => console.log(props.path)} >console.log(path)</button> <button className="btn btn-success" onClick={() => console.table(props.path)} >console.table(path)</button> </div> </div> <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 }) => { 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> <strong>Curve</strong> to {pointCoords(op.to)} <br /> Cp1: {pointCoords(op.cp1)} <br /> 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> } 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)) }} markerStart="none" markerEnd="none" /> {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> ) } const Path = props => { const { path, partName, pathName } = props if (!path.render) return null const output = [] 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)} />) 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} />) return output } export default Path