diff --git a/packages/react-components/src/index.mjs b/packages/react-components/src/index.mjs
index 80ab7b1c173..57f3fadfbea 100644
--- a/packages/react-components/src/index.mjs
+++ b/packages/react-components/src/index.mjs
@@ -1,3 +1,4 @@
+// Components
import { Pattern as PatternComponent } from './pattern/index.mjs'
import { Svg as SvgComponent } from './pattern/svg.mjs'
import { Defs as DefsComponent } from './pattern/defs.mjs'
@@ -9,6 +10,8 @@ import { Snippet as SnippetComponent } from './pattern/snippet.mjs'
import { Path as PathComponent } from './pattern/path.mjs'
import { Grid as GridComponent } from './pattern/grid.mjs'
import { Text as TextComponent, TextOnPath as TextOnPathComponent } from './pattern/text.mjs'
+// Pattern Utils
+import { getProps, withinPartBounds, getId, translateStrings } from './pattern/utils.mjs'
/*
* Export all components as named exports
@@ -25,3 +28,13 @@ export const Snippet = SnippetComponent
export const Grid = GridComponent
export const Text = TextComponent
export const TextOnPath = TextOnPathComponent
+
+/*
+ * Export pattern utils
+ */
+export const utils = {
+ getProps,
+ withinPartBounds,
+ getId,
+ translateStrings,
+}
diff --git a/packages/react-components/src/pattern/stack.mjs b/packages/react-components/src/pattern/stack.mjs
index 7dc7da6956a..b4b51cb0ddb 100644
--- a/packages/react-components/src/pattern/stack.mjs
+++ b/packages/react-components/src/pattern/stack.mjs
@@ -6,12 +6,8 @@ export const Stack = ({ stackName, stack, settings, components, t }) => {
return (
- {[...stack.parts].map((part) => (
-
+ {[...stack.parts].map((part, key) => (
+
))}
)
diff --git a/packages/react-components/src/pattern/utils.mjs b/packages/react-components/src/pattern/utils.mjs
index b2d724d438f..faf9fed0605 100644
--- a/packages/react-components/src/pattern/utils.mjs
+++ b/packages/react-components/src/pattern/utils.mjs
@@ -47,7 +47,7 @@ export const withinPartBounds = (point, part) =>
: false
export const getId = ({
- settings,
+ settings = {},
stackName = false,
partName = false,
pathName = false,
diff --git a/sites/shared/components/mdx/tabbed-example.mjs b/sites/shared/components/mdx/tabbed-example.mjs
index b4d2c4db3a8..c2e50ce812a 100644
--- a/sites/shared/components/mdx/tabbed-example.mjs
+++ b/sites/shared/components/mdx/tabbed-example.mjs
@@ -5,8 +5,8 @@ import { pluginFlip } from '@freesewing/plugin-flip'
import { pluginGore } from '@freesewing/plugin-gore'
import { Design } from '@freesewing/core'
import { Svg } from 'pkgs/react-components/src/index.mjs'
-import { Defs } from '../workbench/pattern/defs'
-import { Stack } from '../workbench/pattern/stack'
+//import { Defs } from '../workbench/pattern/defs'
+//import { Stack } from '../workbench/pattern/stack'
import { useState, useEffect } from 'react'
import yaml from 'js-yaml'
@@ -40,22 +40,26 @@ export const Example = ({
)
- return (
-
- )
+ return null
+
+ // FIXME
+
+ //return (
+ //
+ //)
}
// Returns a FreeSewing draft based on code in children
diff --git a/sites/shared/components/workbench/pattern/point.mjs b/sites/shared/components/workbench/pattern/point.mjs
deleted file mode 100644
index 58b9a1b9425..00000000000
--- a/sites/shared/components/workbench/pattern/point.mjs
+++ /dev/null
@@ -1,108 +0,0 @@
-import { Text } from './text.mjs'
-import { Circle } from './circle.mjs'
-import { Tr, KeyTd, ValTd, Attributes, pointCoords } from './path.mjs'
-import { withinPartBounds } from './utils.mjs'
-
-const RevealPoint = ({ point, pointName, scale, part, partName, ui }) => {
- const r = 15 * scale
- const { x, y } = point
- const { topLeft, bottomRight } = part
- const i = Object.keys(ui.xray.reveal[partName].points).indexOf(pointName) % 10
- const classes = `stroke-sm stroke-color-${i} stroke-dashed`
- return (
-
-
-
-
- )
-}
-const pointInfo = ({ point, pointName, partName }) =>
- point ? (
-
-
-
{JSON.stringify(ui, null, 2)}
- {output}
-
+
{output}
diff --git a/sites/shared/components/workbench/views/draft/inspector/menu.mjs b/sites/shared/components/workbench/views/draft/inspector/menu.mjs
new file mode 100644
index 00000000000..84848c051e2
--- /dev/null
+++ b/sites/shared/components/workbench/views/draft/inspector/menu.mjs
@@ -0,0 +1,49 @@
+// Hooks
+//import { useContext } from 'react'
+import { useTranslation } from 'next-i18next'
+// Context
+//import { ModalContext } from 'shared/context/modal-context.mjs'
+//Dependencies
+//import { loadSettingsConfig } from './config.mjs'
+// Components
+//import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
+import { WrenchIcon, ClearIcon, HelpIcon } from 'shared/components/icons.mjs'
+import { Collapse } from 'shared/components/collapse.mjs'
+
+export const ns = ['inspector']
+
+export const Inspector = ({
+ design,
+ update,
+ settings,
+ patternConfig,
+ language,
+ DynamicDocs,
+ control,
+ ui,
+ inspector,
+}) => {
+ // FIXME: Update this namespace
+ const { t } = useTranslation(ns)
+
+ return (
+ <>
+
+ {control > 4 ? (
+
+ ) : (
+ <>
+
+
+ {t('inspector:inspector')}
+
+
{t('inspector:inspector.d')}
+ >
+ )}
+
+ {Object.values(inspector.data.show).map((props) => (
+
+ ))}
+ >
+ )
+}
diff --git a/sites/shared/components/workbench/views/draft/inspector/pattern.mjs b/sites/shared/components/workbench/views/draft/inspector/pattern.mjs
new file mode 100644
index 00000000000..8e39d1adc01
--- /dev/null
+++ b/sites/shared/components/workbench/views/draft/inspector/pattern.mjs
@@ -0,0 +1,17 @@
+// Hooks
+import { useState, useEffect, useContext } from 'react'
+import { useTranslation } from 'next-i18next'
+// Components
+import { PanZoomPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
+import { Point } from './point.mjs'
+
+export const InspectorPattern = (props) => {
+ const { t } = useTranslation(['workbench'])
+ const inspector = props.inspector
+
+ const components = {
+ Point: (props) =>
,
+ }
+
+ return
+}
diff --git a/sites/shared/components/workbench/views/draft/inspector/point.mjs b/sites/shared/components/workbench/views/draft/inspector/point.mjs
new file mode 100644
index 00000000000..0ed904db60d
--- /dev/null
+++ b/sites/shared/components/workbench/views/draft/inspector/point.mjs
@@ -0,0 +1,191 @@
+// Hooks
+import { useState, useEffect, useContext } from 'react'
+// Context
+import { ModalContext } from 'shared/context/modal-context.mjs'
+// Components
+import { Point as ShowPoint, Text, Circle, utils } from 'pkgs/react-components/src/index.mjs'
+import { Tr, KeyTd, ValTd, Attributes, pointCoords, useInfoLoader, KeyValTable } from './shared.mjs'
+import { round } from 'shared/utils.mjs'
+import { TrashIcon, PrintIcon, SearchIcon } from 'shared/components/icons.mjs'
+
+const { withinPartBounds, getId, getProps } = utils
+
+const RevealPoint = ({ point, pointName, scale, part, id, inspector }) => {
+ const r = 15 * scale
+ const { x, y } = point
+ const { topLeft, bottomRight } = part
+ const classes = `stroke-sm stroke-lining`
+ return (
+
+ inspector.reveal(id)}
+ />
+
+
+ )
+}
+
+const Cross = ({ point, color = 'primary' }) => (
+
+)
+
+const InspectPoint = ({
+ point,
+ part,
+ pointName,
+ stackName,
+ scale = 1,
+ t,
+ inspector,
+ color = 'lining',
+}) => {
+ const id = utils.getId({ stackName, pointName, settings: { idPrefix: 'point-' } })
+ const info = {
+ id,
+ title: (
+
+
+ Point: {pointName} | {stackName}
+
+ {pointCoords(point)}
+
+ ),
+ openTitle: (
+
+ Point: {pointName} | {stackName}
+
+ ),
+ buttons: [
+
,
+ ],
+ openButtons: [
+
,
+
,
+
,
+ ],
+ children: (
+
],
+ ]}
+ />
+ ),
+ color: 'accent',
+ }
+
+ return (
+
+
+
+
+ inspector.show(info)}
+ />
+ {inspector.data.reveal[id] ? (
+
+ ) : null}
+
+ )
+}
+/*
+ title,
+ openTitle = false,
+ children = [],
+ buttons = [],
+ top = true,
+ bottom = false,
+ color = 'primary',
+ opened = false,
+ toggle = false,
+ toggleClasses = '',
+ onClick = false,
+ openButtons = null,
+ className = '',
+*/
+export const Point = ({
+ stackName,
+ pointName,
+ part,
+ point,
+ settings,
+ components,
+ t,
+ ui,
+ update,
+ inspector,
+}) => {
+ const showInfo = useInfoLoader()
+
+ // Don't include parts outside the part bounding box
+ if (!withinPartBounds(point, part)) return null
+
+ const pointProps = { stackName, pointName, part, point, settings, components, t }
+
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/sites/shared/components/workbench/views/draft/inspector/shared.mjs b/sites/shared/components/workbench/views/draft/inspector/shared.mjs
new file mode 100644
index 00000000000..96c754ef11f
--- /dev/null
+++ b/sites/shared/components/workbench/views/draft/inspector/shared.mjs
@@ -0,0 +1,631 @@
+// Hooks
+import { useContext } from 'react'
+// Context
+import { ModalContext } from 'shared/context/modal-context.mjs'
+// Dependencies
+import { round } from 'shared/utils.mjs'
+// Components
+import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
+
+export const useInfoLoader = () => {
+ const { setModal } = useContext(ModalContext)
+
+ const showInfo = (content) => {
+ setModal(
{content})
+ }
+
+ return showInfo
+}
+
+export const KeyValTable = ({ rows }) => (
+
+
+ {rows.map((row) => (
+
+ {row[0]}: |
+ {row[1]} |
+
+ ))}
+
+
+)
+
+export const pointCoords = (point) =>
+ point ? `[ ${round(point.x, 2)}, ${round(point.y, 2)} ]` : null
+
+export const Tr = ({ children }) =>
{children}
+export const KeyTd = ({ children }) =>
{children}: |
+export const ValTd = ({ children }) =>
{children} |
+
+export const TextAlongPath = ({ id, size, txt }) => (
+
+
+
+ {txt}
+
+
+
+)
+export const PointCircle = ({ point, size, className = 'stroke-neutral-content' }) => (
+
+)
+
+const CpCircle = ({ point, className = 'fill-lining no-stroke' }) => (
+
+)
+
+const EpCircle = ({ point, className = 'fill-note no-stroke' }) => (
+
+)
+
+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.bbox()
+ }
+ 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 = () => (
+
+
+
+
+
+
+
+
+)
+
+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 = ({ path, pathName, partName, i, units }) => {
+ const ops = path.ops
+ const from = ops[0].to
+ const to = ops[1].to
+ const { topLeft, bottomRight, w, h, size } = pathDimensions(from, to)
+ const id = `${partName}_${pathName}_${i}`
+
+ const xyProps = {
+ className: 'stroke-neutral-content',
+ strokeOpacity: '0.5',
+ fill: 'none',
+ strokeWidth: size / 300,
+ }
+
+ return (
+
+ )
+}
+
+const Curve = ({ path, pathName, partName, i }) => {
+ const ops = path.ops
+ const from = ops[0].to
+ const { to, cp1, cp2 } = ops[1]
+ const { topLeft, w, h, size, bbox } = pathDimensions(from, to, cp1, cp2, path)
+ const id = `${partName}_${pathName}_${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 (
+
+ )
+}
+
+const MiniPath = ({ path, pathName, partName, units }) => {
+ const bbox = path.bbox()
+ const id = `${partName}_${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 (
+
+ )
+}
+
+const lineInfo = ({ path, partName, units, i }) => (
+
+
Line info
+
+
+
+
+ From
+ {pointCoords(path.ops[0].to)}
+
+
+ To
+ {pointCoords(path.ops[1].to)}
+
+
+ Length
+ {formatMm(path.length(), units, 'notags')}
+
+
+ Width
+
+
+
+
+
+ Height
+
+
+
+
+
+ Part
+ {partName}
+
+
+ Draw Op
+
+ {i}/{ops.length}
+
+
+
+
+
+
+
+
+
+)
+
+const XrayLine = ({ i, path, partName, units, showInfo }) => (
+ <>
+
showInfo(evt, lineInfo({ path, partName, units, i }))}
+ />
+
+
+ >
+)
+
+const curveInfo = ({ i, path, partName, units }) => (
+
+
Curve info
+
+
+
+
+ From
+ {pointCoords(path.ops[0].to)}
+
+
+ Cp1
+ {pointCoords(path.ops[1].cp1)}
+
+
+ Cp2
+ {pointCoords(path.ops[1].cp2)}
+
+
+ To
+ {pointCoords(path.ops[1].to)}
+
+
+ Length
+
+
+
+
+
+ Width
+
+
+
+
+
+ Height
+
+
+
+
+
+ Part
+ {partName}
+
+
+ Draw Op
+
+ {i}/{ops.length}
+
+
+
+
+
+
+
+
+
+)
+
+export const Attributes = ({ list }) =>
+ list ? (
+
+ {Object.keys(list).map((key) => (
+ -
+ {key}: {list[key]}
+
+ ))}
+
+ ) : null
+
+export const pathInfo = ({ pathName, path, partName, units, showInfo }) => {
+ const { t } = useTranslation(['workbench'])
+ const bbox = path.bbox()
+
+ return (
+
+
{t('pathInfo')}
+
+
+
+
+
+ {t('name')}
+ {pathName}
+
+
+ {t('length')}
+
+
+
+
+
+ {t('width')}
+
+
+
+
+
+ {t('height')}
+
+
+
+
+
+ {t('topLeft')}
+ {pointCoords(bbox.topLeft)}
+
+
+ {t('bottomRight')}
+ {pointCoords(bbox.bottomRight)}
+
+
+ {t('part')}
+ {partName}
+
+
+ {t('attributes')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {path.ops.map((op, i) => (
+
+ {i}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
+
+const PathOp = ({ op }) => {
+ if (op.type === 'move')
+ return (
+
+ Move to {pointCoords(op.to)}
+
+ )
+ else if (op.type === 'line')
+ return (
+
+ Line to {pointCoords(op.to)}
+
+ )
+ else if (op.type === 'curve')
+ return (
+
+ Curve to {pointCoords(op.to)}
+
+ Cp1: {pointCoords(op.cp1)}
+
+ Cp2: {pointCoords(op.cp2)}
+
+ )
+ else if (op.type === 'noop') return NOOP
+ else if (op.type === 'close') return Close
+ else return FIXME: unknown path operation type: {op.type}
+}
+
+const XrayCurve = ({ i, path, ops, units, showInfo }) => {
+ const from = path.ops[0].to
+ const { cp1, cp2, to } = path.ops[1]
+
+ return (
+ <>
+ showInfo(evt, curveInfo({ i, path, partName, units }))}
+ />
+
+
+
+
+
+
+ >
+ )
+}
+
+const XrayPath = ({ path, pathName, partName, units, showInfo }) => {
+ const classes = path.attributes.get('class')
+ if (typeof classes === 'string' && classes.includes('noxray')) return null
+ const ops = path.divide()
+
+ return (
+
+ showInfo(evt, pathInfo({ pathName, path, partName, units }))}
+ markerStart="none"
+ markerEnd="none"
+ />
+ {ops.length > 0
+ ? ops.map((op, i) =>
+ op.ops[1].type === 'curve' ? (
+
+ ) : (
+
+ )
+ )
+ : null}
+
+ )
+}
+
+export const Path = ({ pathName, path, partName, part, units, showInfo, ui, update }) => {
+ if (path.hidden) 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()
+ if (path.attributes.get('data-text'))
+ output.push(
+
+ )
+ if (ui.xray?.enabled) output.push()
+
+ return output
+}
diff --git a/sites/shared/components/workbench/views/draft/menu.mjs b/sites/shared/components/workbench/views/draft/menu.mjs
index bebbcde673f..8fc208461d6 100644
--- a/sites/shared/components/workbench/views/draft/menu.mjs
+++ b/sites/shared/components/workbench/views/draft/menu.mjs
@@ -7,8 +7,9 @@ import {
ns as coreMenuNs,
} from 'shared/components/workbench/menus/core-settings/index.mjs'
import { UiSettings, ns as uiNs } from 'shared/components/workbench/menus/ui-settings/index.mjs'
+import { Inspector, ns as inspectorNs } from './inspector/menu.mjs'
-export const ns = [...coreMenuNs, ...designMenuNs, ...uiNs]
+export const ns = [...coreMenuNs, ...designMenuNs, ...uiNs, inspectorNs]
export const DraftMenu = ({
design,
@@ -19,6 +20,7 @@ export const DraftMenu = ({
language,
account,
DynamicDocs,
+ inspector = false,
}) => {
// Default control level is 2 (in case people are not logged in)
const control = account.control || 2
@@ -35,6 +37,7 @@ export const DraftMenu = ({
return (