Merge branch 'joost' into plugins-scale
This commit is contained in:
commit
d739e8f5bd
24466 changed files with 405611 additions and 707715 deletions
129
sites/shared/components/workbench/de.yaml
Normal file
129
sites/shared/components/workbench/de.yaml
Normal file
|
@ -0,0 +1,129 @@
|
|||
addNotes: Add notes
|
||||
addSettingsToNotes: Add settings to notes
|
||||
advanced: Fortgeschritten
|
||||
appliedMeasies: Wir haben dieses Schnittmuster mit einem neuen Maßsatz versehen.
|
||||
armhole: Armloch
|
||||
attributes: Eigenschaften
|
||||
backPockets: Gesäßtaschen
|
||||
bookmarkPattern: Bookmark pattern
|
||||
bottomRight: Unten rechts
|
||||
changeMeasies: Muster ändern Messungen
|
||||
chooseATest: Choose a test scenario to run
|
||||
chooseATestDesc: You can test how the design adapts to changes in a specific design option or measurements.
|
||||
chooseATestMenuMobileMsg: To do so, open the test menu and select the scenario you want to run.
|
||||
chooseATestMenuMsg: To do so, select the test scenario you want to run in the menu on the right.
|
||||
chooseFromBookmarkedSets: Choose one of the measurements sets you've bookmarked
|
||||
chooseFromBookmarkedSetsDesc: If you've bookmarked any measurements sets, you can select from those too.
|
||||
chooseFromCuratedSets: Choose one of FreeSewing's curated measurements sets
|
||||
chooseFromCuratedSetsDesc: If you're just looking to try out our platform, you can select from our list of curated measurements sets.
|
||||
chooseFromOwnSets: Choose one of your own measurements sets
|
||||
chooseFromOwnSetsDesc: Pick any of your own measurements sets that have all required measurements to generate this pattern.
|
||||
chooseNewSet: Wähle ein neues Maßnahmeset
|
||||
closure: Verschluss
|
||||
collar: Kragen
|
||||
columns: columns
|
||||
configurePattern: Muster konfigurieren
|
||||
construction: Konstruktion
|
||||
continueEditingTitle: Continue editing
|
||||
continueEditingDesc: Load the newly saved pattern in the pattern editor, so you can make more changes.
|
||||
cuffs: Manschetten
|
||||
currentPrintLayout: Current print layout
|
||||
cutLayout: Layout schneiden
|
||||
darts: Abnäher
|
||||
designOptions.d: Teste die Auswirkungen einer Option auf das Aussehen dieses Musters
|
||||
docs: Dokumentation
|
||||
draft: Entwurf
|
||||
draftPattern: Entwurfsmuster
|
||||
edit: bearbeiten
|
||||
editCurrentMeasies: Aktuelle Maße bearbeiten
|
||||
editCurrentMeasiesDesc: Änderungen, die du hier vornimmst, werden nicht in deine Maßsätze gespeichert und wirken sich nur auf dieses Schnittmuster aus.
|
||||
editCurrentMeasiesHeader: Schnittmuster-Maße bearbeiten
|
||||
editMeasiesByHand: Edit measurements by hand
|
||||
editMeasiesByHandDesc: Manually set or override any measurements. These changes will only apply to the current pattern.
|
||||
editSettings: Konfiguration bearbeiten
|
||||
elastic: Gummi
|
||||
export: Exportieren
|
||||
exportAsData: Als Daten exportieren
|
||||
exportForEditing: Für die Bearbeitung exportieren
|
||||
exportForPrinting: Für den Druck exportieren
|
||||
exportPattern-txt: Ein für deinen Heimdrucker geeignetes PDF exportieren, oder das Schnittmuster in verschiedenen Formaten herunterladen
|
||||
exportPattern: Schnittmuster exportieren
|
||||
fit: Passform
|
||||
frontPockets: Vordere Taschen
|
||||
generatePdf: Generate print-ready PDF
|
||||
giveItAName: Gib ihm einen Namen
|
||||
goToPatternTitle: Navigate to the pattern page
|
||||
goToPatternDesc: Exit the pattern editor and navigate to the pattern page where you can update the pattern's metadata.
|
||||
height: Höhe
|
||||
help: Hilfe
|
||||
layoutSettings.d: Additional options to further optimize the printing layout of your pattern.
|
||||
layoutSettings.t: Layout settings
|
||||
length: Länge
|
||||
learnHowToUseEditor: Learn how to use FreeSewing's online pattern editor
|
||||
measies: Muster-Messungen
|
||||
measiesOk: Wir haben alle erforderlichen Maße, um dieses Muster zu erstellen.
|
||||
measurements.d: Teste die Auswirkungen einer Messung auf das Aussehen des Musters
|
||||
menu: Menü
|
||||
name: Name
|
||||
noDesignFound: Wir konnten dieses Design nicht finden. Das sollte nicht passieren, also würden wir uns freuen, wenn du es meldest.
|
||||
noInlineDocs: Documention is not included in this build
|
||||
noInlineDocsDesc: Please visit FreeSewing.org to access our documentation
|
||||
notes: Notizen
|
||||
pages: pages
|
||||
part: Musterteil
|
||||
partInfo: Musterteil Info
|
||||
partTransfo: Part transformation buttons
|
||||
partTransfoDesc: These buttons allow you to rotate or flip individual pattern parts.
|
||||
partTransfoNo: Hide buttons
|
||||
partTransfoNoDesc: Do not include these buttons on the pattern output
|
||||
partTransfoYes: Show buttons
|
||||
partTransfoYesDesc: Include these buttons on the pattern output (they will not be printed)
|
||||
pathInfo: Pfad-Infos
|
||||
patternBookmarkCreated: Pattern bookmark created
|
||||
patternInspector: Muster-Inspektor
|
||||
patternLogs: Musterprotokolle
|
||||
patternSaved: Pattern saved
|
||||
pockets: Taschen
|
||||
printLayout: Layout drucken
|
||||
printSettings.d: Configure your pattern so you can print it just the way you like it. Includes page size & orientation, margins, and more.
|
||||
printSettings.t: Print settings
|
||||
reset: Zurücksetzen
|
||||
resetPrintLayout: Reset print layout
|
||||
resetPrintLayoutDesc: Removes all manual changes to the print layout, and restores the default layout
|
||||
rows: rows
|
||||
save: Speichern
|
||||
savePattern: Schnittmuster speichern
|
||||
saveAsNewPattern: Save as new pattern
|
||||
savePatternAs: Save pattern as...
|
||||
savePatternAsHellip: Save pattern as...
|
||||
saveSettings: Einstellungen speichern
|
||||
saveYourPattern: Speichere dein Schnittmuster
|
||||
seeMissingMeasies: Siehe fehlende Maße
|
||||
show: Anzeigen
|
||||
showAllParts: Alle Musterteile anzeigen
|
||||
showMovableButtons: Knöpfe
|
||||
showOnlyThisPart: Nur diesen Teil des Musters anzeigen
|
||||
sleevecap: Armkugel
|
||||
style: Stil
|
||||
test: Test
|
||||
testDesignMeasurement: "Test {design} measurements: {measurement}"
|
||||
testDesignOption: "Test {design} design options: {option}"
|
||||
testDesignSets: "Test {design} across measurements sets"
|
||||
testMeasurements: Test measurements
|
||||
testMeasurementsDesc: Test how the design adapts to changes to a specific measurement
|
||||
testOptions: Test design options
|
||||
testOptionsDesc: Test how the design adapts to changes in a specific design option
|
||||
testPattern: Testmuster
|
||||
testSets: Test measurements sets
|
||||
testSetsDesc: Test how the design adapts across different measurements sets
|
||||
topLeft: Oben links
|
||||
weLackSomeMeasies: Uns fehlen { nr } Messungen, um dieses Muster zu erstellen
|
||||
whereToGoAfterSaveAs: After saving the new pattern, what do you want to do?
|
||||
width: Breite
|
||||
xTotalPagesSomeBlank: "{total} pages in total but {blank} are blank"
|
||||
yamlEditViewError: Probleme mit YAML
|
||||
yamlEditViewErrorDesc: Wir haben deine Eingabe gespeichert, aber sie funktioniert möglicherweise aus folgenden Gründen nicht
|
||||
yamlEditViewTitleThing: 'Musterkonfiguration bearbeiten für {thing}'
|
||||
youCanPickOrEnter: Du kannst entweder einen Satz Messungen auswählen oder sie von Hand eingeben, aber ohne diese Messungen können wir nicht weitermachen.
|
||||
youUseCustomValue: You are using the default value
|
||||
youUseDefaultValue: You are using a custom value
|
|
@ -1,14 +0,0 @@
|
|||
export const Circle = (props) =>
|
||||
props.point.attributes.getAsArray('data-circle').map((r, i) => {
|
||||
const circleProps = props.point.attributes.asPropsIfPrefixIs('data-circle-')
|
||||
const extraProps = {}
|
||||
for (const prop in circleProps) {
|
||||
const val = props.point.attributes.getAsArray(
|
||||
`data-circle-${prop === 'className' ? 'class' : prop}`
|
||||
)
|
||||
if (val.length >= i) extraProps[prop] = val[i]
|
||||
else extraProps[prop] = val.join(' ')
|
||||
}
|
||||
|
||||
return <circle key={r} cx={props.point.x} cy={props.point.y} r={r} {...extraProps} />
|
||||
})
|
|
@ -1,67 +0,0 @@
|
|||
const style = ` style="fill: none; stroke: currentColor;" `
|
||||
const grids = {
|
||||
imperial: `
|
||||
<pattern id="grid" height="25.4" width="25.4" patternUnits="userSpaceOnUse" key="grid">
|
||||
<path class="gridline lg imperial" d="M 0 0 L 0 25.4 L 25.4 25.4" ${style} />
|
||||
<path
|
||||
class="gridline lg imperial"
|
||||
d="M 12.7 0 L 12.7 25.4 M 0 12.7 L 25.4 12.7"
|
||||
${style}
|
||||
/>
|
||||
<path
|
||||
class="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"
|
||||
${style}
|
||||
/>
|
||||
<path
|
||||
class="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"
|
||||
${style}
|
||||
/>
|
||||
</pattern>
|
||||
`,
|
||||
metric: `
|
||||
<pattern id="grid" height="100" width="100" patternUnits="userSpaceOnUse" key="grid">
|
||||
<path class="gridline lg metric" d="M 0 0 L 0 100 L 100 100" ${style} />
|
||||
<path class="gridline metric" d="M 50 0 L 50 100 M 0 50 L 100 50" ${style} />
|
||||
<path
|
||||
class="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"
|
||||
${style}
|
||||
/>
|
||||
<path
|
||||
class="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"
|
||||
${style}
|
||||
/>
|
||||
<path
|
||||
class="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"
|
||||
${style}
|
||||
/>
|
||||
<path
|
||||
class="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"
|
||||
${style}
|
||||
/>
|
||||
</pattern>
|
||||
`,
|
||||
}
|
||||
|
||||
export const Defs = (props) => {
|
||||
let defs = props.svg.defs.render()
|
||||
if (props.settings[0].paperless) {
|
||||
defs += grids[props.settings[0].units || 'metric']
|
||||
for (let p in props.parts[0]) {
|
||||
let anchor = { x: 0, y: 0 }
|
||||
if (typeof props.parts[0][p].points.gridAnchor !== 'undefined')
|
||||
anchor = props.parts[0][p].points.gridAnchor
|
||||
else if (typeof props.parts[0][p].points.anchor !== 'undefined')
|
||||
anchor = props.parts[0][p].points.anchor
|
||||
|
||||
defs += `<pattern id="grid-${p}" key="grid-${p}" xlink:href="#grid" x="${anchor.x}" y="${anchor.y}" />`
|
||||
}
|
||||
}
|
||||
|
||||
return <defs dangerouslySetInnerHTML={{ __html: defs }} />
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import { ErrorView } from 'shared/components/error/view.mjs'
|
||||
|
||||
export const DraftError = (props) => {
|
||||
const errors = {
|
||||
pattern: 0,
|
||||
sets: 0,
|
||||
}
|
||||
const warnings = {
|
||||
pattern: 0,
|
||||
sets: 0,
|
||||
}
|
||||
if (props.patternLogs) {
|
||||
errors.pattern += props.patternLogs.error.length
|
||||
warnings.pattern += props.patternLogs.warning.length
|
||||
}
|
||||
if (props.setLogs) {
|
||||
errors.sets += props.setLogs.error.length
|
||||
warnings.sets += props.setLogs.warning.length
|
||||
}
|
||||
|
||||
const logInfo = []
|
||||
if (errors.pattern > 0)
|
||||
logInfo.push(
|
||||
<li>
|
||||
There are <strong>{errors.pattern} errors</strong> in the pattern logs
|
||||
</li>
|
||||
)
|
||||
if (errors.sets > 0)
|
||||
logInfo.push(
|
||||
<li>
|
||||
There are <strong>{errors.sets} errors</strong> in the draft logs
|
||||
</li>
|
||||
)
|
||||
if (warnings.pattern > 0)
|
||||
logInfo.push(
|
||||
<li>
|
||||
There are <strong>{warnings.pattern} warnings</strong> in the pattern logs
|
||||
</li>
|
||||
)
|
||||
if (warnings.sets > 0)
|
||||
logInfo.push(
|
||||
<li>
|
||||
There are <strong>{warnings.sets} warnings</strong> in the draft logs
|
||||
</li>
|
||||
)
|
||||
const ic = (
|
||||
<ul className="list-disc list-inside ml-4 text-xl">
|
||||
{logInfo}
|
||||
{logInfo.length > 0 && (
|
||||
<li>
|
||||
<button className="btn-link" onClick={() => props.updateGist(['_state', 'view'], 'logs')}>
|
||||
Check the logs for more details
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
<li>Check the partially rendered pattern below to see which areas are problematic</li>
|
||||
</ul>
|
||||
)
|
||||
|
||||
return (
|
||||
<ErrorView inspectChildren={ic} {...props}>
|
||||
<p>No need to be alarmed, but we ran into some trouble while drafting this pattern.</p>
|
||||
</ErrorView>
|
||||
)
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import { SvgWrapper } from './svg.mjs'
|
||||
import { DraftError } from './error.mjs'
|
||||
|
||||
export const LabDraft = (props) => {
|
||||
const { app, draft, gist, updateGist, unsetGist, showInfo, feedback, hasRequiredMeasurements } =
|
||||
props
|
||||
|
||||
if (!draft || !hasRequiredMeasurements) return null
|
||||
|
||||
// Render as SVG
|
||||
if (gist?.renderer === 'svg') {
|
||||
let svg
|
||||
try {
|
||||
svg = draft.render()
|
||||
} catch (error) {
|
||||
console.log('Failed to render design', error)
|
||||
return <DraftError error={error} {...props} />
|
||||
}
|
||||
return <div dangerouslySetInnerHTML={{ __html: svg }} />
|
||||
}
|
||||
|
||||
// Render as React
|
||||
let patternProps = {}
|
||||
try {
|
||||
patternProps = draft.getRenderProps()
|
||||
} catch (error) {
|
||||
console.log('Failed to get render props for design', error)
|
||||
return (
|
||||
<DraftError
|
||||
error={error}
|
||||
patternLogs={patternProps.store.logs}
|
||||
setLogs={patternProps.setStores[0].logs}
|
||||
updateGist={updateGist}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const errors = []
|
||||
errors.push(...patternProps.logs.pattern.error)
|
||||
for (const set of patternProps.logs.sets) {
|
||||
errors.push(...set.error)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{errors.length > 0 ? (
|
||||
<DraftError
|
||||
{...{
|
||||
draft,
|
||||
patternProps,
|
||||
updateGist,
|
||||
patternLogs: draft.store.logs,
|
||||
setLogs: draft.setStores[0].logs,
|
||||
errors,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<SvgWrapper
|
||||
{...{ draft, patternProps, gist, updateGist, unsetGist, showInfo, app, feedback }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { Point } from './point.mjs'
|
||||
import { Snippet } from './snippet.mjs'
|
||||
import { getProps } from './utils.mjs'
|
||||
import { round } from 'shared/utils.mjs'
|
||||
import { Path, Tr, KeyTd, ValTd, Attributes, pointCoords } from './path.mjs'
|
||||
|
||||
const partInfo = (props) => (
|
||||
<div className="p-4 border bg-neutral bg-opacity-40 shadow rounded-lg">
|
||||
<h5 className="text-neutral-content text-center pb-4">Part info</h5>
|
||||
<table className="border-collapse h-fit">
|
||||
<tbody>
|
||||
<Tr>
|
||||
<KeyTd>Name</KeyTd>
|
||||
<ValTd>{props.partName}</ValTd>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<KeyTd>Width</KeyTd>
|
||||
<ValTd>{round(props.part.width, 2)}mm</ValTd>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<KeyTd>Height</KeyTd>
|
||||
<ValTd>{round(props.part.height, 2)}mm</ValTd>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<KeyTd>Top Left</KeyTd>
|
||||
<ValTd>{pointCoords(props.part.topLeft)}</ValTd>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<KeyTd>Bottom Right</KeyTd>
|
||||
<ValTd>{pointCoords(props.part.bottomRight)}</ValTd>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<KeyTd>Attributes</KeyTd>
|
||||
<ValTd>
|
||||
<Attributes list={props.part.attributes.list} />
|
||||
</ValTd>
|
||||
</Tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-row flex-wrap gap-2 mt-4">
|
||||
{props.gist?.only && props.gist.only.length > 0 ? (
|
||||
<button className="btn btn-primary" onClick={() => props.unsetGist(['only'])}>
|
||||
Show all parts
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => props.updateGist(['only'], [props.partName])}
|
||||
>
|
||||
Show only this part
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-success" onClick={() => console.log(props.part)}>
|
||||
console.log(part)
|
||||
</button>
|
||||
<button className="btn btn-success" onClick={() => console.table(props.part.points)}>
|
||||
console.table(part.points)
|
||||
</button>
|
||||
<button className="btn btn-success" onClick={() => console.table(props.part.paths)}>
|
||||
console.table(part.paths)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const XrayPart = (props) => {
|
||||
const { topLeft, bottomRight } = props.part
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
d={`
|
||||
M ${topLeft.x} ${topLeft.y}
|
||||
L ${topLeft.x} ${bottomRight.y}
|
||||
L ${bottomRight.x} ${bottomRight.y}
|
||||
L ${bottomRight.x} ${topLeft.y}
|
||||
z
|
||||
`}
|
||||
className={`peer stroke-note lashed opacity-30 hover:opacity-90 fill-fabric hover:cursor-pointer hover:stroke-mark`}
|
||||
style={{ fillOpacity: 0 }}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation()
|
||||
props.showInfo(partInfo(props))
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export const PartInner = forwardRef((props, ref) => {
|
||||
const { partName, part, gist, skipGrid } = props
|
||||
|
||||
const Grid =
|
||||
gist.paperless && !skipGrid ? (
|
||||
<rect
|
||||
x={part.topLeft.x}
|
||||
y={part.topLeft.y}
|
||||
width={part.width}
|
||||
height={part.height}
|
||||
className="grid"
|
||||
fill={'url(#grid-' + partName + ')'}
|
||||
/>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<g ref={ref}>
|
||||
{Grid}
|
||||
{gist._state?.xray?.enabled && <XrayPart {...props} />}
|
||||
{Object.keys(part.paths).map((pathName) => (
|
||||
<Path
|
||||
key={pathName}
|
||||
pathName={pathName}
|
||||
path={part.paths[pathName]}
|
||||
topLeft={props.part.topLeft}
|
||||
bottomRight={props.part.bottomRight}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
{Object.keys(props.part.points).map((pointName) => (
|
||||
<Point
|
||||
key={pointName}
|
||||
pointName={pointName}
|
||||
point={props.part.points[pointName]}
|
||||
topLeft={props.part.topLeft}
|
||||
bottomRight={props.part.bottomRight}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
{Object.keys(props.part.snippets).map((snippetName) => (
|
||||
<Snippet
|
||||
key={snippetName}
|
||||
snippetName={snippetName}
|
||||
snippet={props.part.snippets[snippetName]}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
})
|
||||
|
||||
PartInner.displayName = 'PartInner'
|
||||
|
||||
export const Part = (props) => {
|
||||
const { partName, part } = props
|
||||
|
||||
return (
|
||||
<g
|
||||
{...getProps(part)}
|
||||
id={`${part.context.settings.idPrefix || ''}part-${partName}`}
|
||||
className={part.context.settings.idPrefix || ''}
|
||||
>
|
||||
<PartInner {...props} />
|
||||
</g>
|
||||
)
|
||||
}
|
|
@ -1,657 +0,0 @@
|
|||
import { TextOnPath } from './text.mjs'
|
||||
import { getProps } from './utils.mjs'
|
||||
import { round, formatMm } from 'shared/utils.mjs'
|
||||
import { RawSpan } from 'shared/components/raw-span.mjs'
|
||||
|
||||
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, 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 EpCircle = ({ point, className = 'fill-note 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.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 = () => (
|
||||
<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.bbox()
|
||||
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))
|
||||
}}
|
||||
/>
|
||||
<EpCircle point={props.path.ops[0].to} />
|
||||
<EpCircle point={props.path.ops[1].to} />
|
||||
</>
|
||||
)
|
||||
|
||||
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.bbox()
|
||||
|
||||
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>{props.pathName}</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={props.path.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} />
|
||||
<EpCircle point={from} />
|
||||
<EpCircle point={to} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const XrayPath = (props) => {
|
||||
const classes = props.path.attributes.get('class')
|
||||
if (typeof classes === 'string' && classes.includes('noxray')) return null
|
||||
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`}
|
||||
key={i}
|
||||
/>
|
||||
) : (
|
||||
<XrayLine
|
||||
{...props}
|
||||
path={op}
|
||||
ops={ops}
|
||||
i={i}
|
||||
pathName={`${props.pathName}_test`}
|
||||
key={i}
|
||||
/>
|
||||
)
|
||||
)
|
||||
: null}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export const Path = (props) => {
|
||||
const { path, partName, pathName } = props
|
||||
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(<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
|
||||
}
|
|
@ -1,105 +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 = (props) => {
|
||||
const r = 15 * props.gist.scale
|
||||
const { x, y } = props.point
|
||||
const { topLeft, bottomRight } = props.part
|
||||
const i =
|
||||
Object.keys(props.gist._state.xray.reveal[props.partName].points).indexOf(props.pointName) % 10
|
||||
const classes = `stroke-sm stroke-color-${i} stroke-dashed`
|
||||
return (
|
||||
<g>
|
||||
<circle cx={x} cy={y} r={r} className={classes} />
|
||||
<path
|
||||
d={`
|
||||
M ${x} ${topLeft.y} L ${x} ${y - r}
|
||||
m 0 ${2 * r} L ${x} ${bottomRight.y}
|
||||
M ${topLeft.x} ${y} L ${x - r} ${y}
|
||||
m ${2 * r} 0 L ${bottomRight.x} ${y}`}
|
||||
className={classes}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
const pointInfo = (props) =>
|
||||
props.point ? (
|
||||
<div className="p-4 border bg-neutral bg-opacity-60 shadow rounded-lg">
|
||||
<h5 className="text-neutral-content text-center pb-4">Point info</h5>
|
||||
<table className="border-collapse h-fit">
|
||||
<tbody>
|
||||
<Tr>
|
||||
<KeyTd>Coordinates</KeyTd>
|
||||
<ValTd>{pointCoords(props.point)}</ValTd>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<KeyTd>Name</KeyTd>
|
||||
<ValTd>{props.pointName}</ValTd>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<KeyTd>Part</KeyTd>
|
||||
<ValTd>{props.partName}</ValTd>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<KeyTd>Attributes</KeyTd>
|
||||
<ValTd>
|
||||
<Attributes list={props.point.attributes.list} />
|
||||
</ValTd>
|
||||
</Tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col flex-wrap gap-2 mt-4">
|
||||
<button className="btn btn-success" onClick={() => console.log(props.point)}>
|
||||
console.log(point)
|
||||
</button>
|
||||
<button className="btn btn-success" onClick={() => console.table(props.point)}>
|
||||
console.table(point)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
const XrayPoint = (props) => (
|
||||
<g>
|
||||
<circle
|
||||
cx={props.point.x}
|
||||
cy={props.point.y}
|
||||
r={2 * props.gist.scale}
|
||||
className="stroke-sm stroke-lining fill-lining fill-opacity-25"
|
||||
/>
|
||||
<circle
|
||||
cx={props.point.x}
|
||||
cy={props.point.y}
|
||||
r={7.5 * props.gist.scale}
|
||||
className="opacity-0 stroke-lining fill-lining hover:opacity-25 hover:cursor-pointer"
|
||||
onClick={(evt) => {
|
||||
props.showInfo(pointInfo(props))
|
||||
evt.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
|
||||
export const Point = (props) => {
|
||||
const { point, pointName, partName, gist, part } = props
|
||||
// Don't include parts outside the part bounding box
|
||||
if (!withinPartBounds(point, part)) return null
|
||||
const output = []
|
||||
if (gist._state?.xray?.enabled) {
|
||||
// Xray for points
|
||||
output.push(<XrayPoint {...props} key={'xp-' + pointName} />)
|
||||
// Reveal (based on clicking the seach icon in sidebar
|
||||
if (gist._state?.xray?.reveal?.[partName]?.points?.[pointName])
|
||||
output.push(<RevealPoint {...props} key={'rp-' + pointName} />)
|
||||
}
|
||||
// Render text
|
||||
if (point.attributes && point.attributes.get('data-text'))
|
||||
output.push(<Text {...props} key={'point-' + pointName} />)
|
||||
// Render circle
|
||||
if (point.attributes && point.attributes.get('data-circle'))
|
||||
output.push(<Circle point={point} key={'circle-' + pointName} />)
|
||||
|
||||
return output.length < 1 ? null : output
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { Part } from './part.mjs'
|
||||
import { getProps } from './utils.mjs'
|
||||
|
||||
export const Stack = (props) => {
|
||||
const { stack, gist, updateGist, unsetGist, showInfo } = props
|
||||
|
||||
return (
|
||||
<g {...getProps(stack)}>
|
||||
{[...stack.parts].map((part) => (
|
||||
<Part
|
||||
{...{ gist, updateGist, unsetGist, showInfo }}
|
||||
key={part.name}
|
||||
partName={part.name}
|
||||
part={part}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { SizeMe } from 'react-sizeme'
|
||||
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'
|
||||
import { Defs } from './defs.mjs'
|
||||
import { Stack } from './stack.mjs'
|
||||
|
||||
export const Svg = forwardRef(
|
||||
(
|
||||
{
|
||||
embed = true,
|
||||
develop = false,
|
||||
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'
|
||||
}
|
||||
if (develop) attributes.className += ' develop'
|
||||
|
||||
return (
|
||||
<svg {...attributes} ref={ref}>
|
||||
{children}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Svg.displayName = 'Svg'
|
||||
|
||||
/* What's with all the wrapping?
|
||||
*
|
||||
* Glad you asked. The problem lies within the pan & zoom functionality
|
||||
* For this to work, we need to reliably determine the container widht
|
||||
* However, our SVG is optimized for embedding (no width or height)
|
||||
* which means it will simply adapt to the parent container.
|
||||
* So now we have a parent container adapting to the content and the
|
||||
* content adapting to the parent. This creates a sort of stalemate
|
||||
* where the browser will render this as zero width and height.
|
||||
*
|
||||
* To avoid that, we use the SizeMe which will report the size of the
|
||||
* grandparent element, and then we wrap our SVG in a div that we
|
||||
* set to this size. This will cause the SVG to fill in that entire
|
||||
* space, and the pan and zoom to adapt to this size.
|
||||
*
|
||||
* Note that this also means that changing the browser window size
|
||||
* will cause things to get clipped until the next render.
|
||||
*
|
||||
* Also still to see how this will work with SSR
|
||||
*/
|
||||
|
||||
export const SvgWrapper = forwardRef((props, ref) => {
|
||||
const { patternProps = false, gist, updateGist, unsetGist, showInfo, viewBox } = props
|
||||
|
||||
if (!patternProps) return null
|
||||
|
||||
return (
|
||||
<SizeMe>
|
||||
{({ size }) => (
|
||||
<TransformWrapper
|
||||
minScale={0.1}
|
||||
centerZoomedOut={true}
|
||||
wheel={{ activationKeys: ['Control'] }}
|
||||
>
|
||||
<TransformComponent>
|
||||
<div style={{ width: size.width + 'px' }}>
|
||||
<Svg {...patternProps} viewBox={viewBox} embed={gist.embed} ref={ref}>
|
||||
<Defs {...patternProps} />
|
||||
<style>{`:root { --pattern-scale: ${gist.scale || 1}} ${
|
||||
patternProps.svg.style
|
||||
}`}</style>
|
||||
<g>
|
||||
{props.children ||
|
||||
Object.keys(patternProps.stacks).map((stackName) => (
|
||||
<Stack
|
||||
{...{ gist, updateGist, unsetGist, showInfo, patternProps }}
|
||||
key={stackName}
|
||||
stackName={stackName}
|
||||
stack={patternProps.stacks[stackName]}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
</Svg>
|
||||
</div>
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
)}
|
||||
</SizeMe>
|
||||
)
|
||||
})
|
||||
|
||||
SvgWrapper.displayName = 'SvgWrapper'
|
|
@ -1,116 +0,0 @@
|
|||
import yaml from 'js-yaml'
|
||||
import { defaultGist } from 'shared/components/workbench/gist.mjs'
|
||||
import { validateGist } from './gist-validator.mjs'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { Popout } from 'shared/components/popout.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { capitalize } from '@freesewing/core'
|
||||
|
||||
/** a view for editing the gist as yaml */
|
||||
export const EditYaml = (props) => {
|
||||
let { gist, setGist, gistReady } = props
|
||||
|
||||
const inputRef = useRef(null)
|
||||
// the gist parsed to yaml
|
||||
const [gistAsYaml, setGistAsYaml] = useState(null)
|
||||
// any errors as a json string
|
||||
const [error, setError] = useState(null)
|
||||
// success notifier
|
||||
const [success, setSuccess] = useState(null)
|
||||
|
||||
const { t } = useTranslation(['workbench'])
|
||||
|
||||
// parse the current gist to yaml. this will also run when the gist gets set by input
|
||||
useEffect(() => {
|
||||
if (gistReady) {
|
||||
// get everything but the design because it's a function and can't be serialized
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { design, ...gistRest } = gist
|
||||
setGistAsYaml(yaml.dump(gistRest))
|
||||
}
|
||||
}, [gist, gistReady])
|
||||
|
||||
// set the line numbers when the yaml changes
|
||||
useEffect(() => {
|
||||
if (gistAsYaml) {
|
||||
inputRef.current.value = gistAsYaml
|
||||
}
|
||||
}, [gistAsYaml])
|
||||
|
||||
/** user-initiated save */
|
||||
const onSave = () => {
|
||||
// clear the errors
|
||||
setError(null)
|
||||
try {
|
||||
// parse back to json
|
||||
const editedAsJson = yaml.load(inputRef.current.value)
|
||||
// make it backwards compatible so that people can paste in the yaml export from org
|
||||
// the yaml export from org is missing some of the settings that are needed in the gist,
|
||||
// and what it does have is under 'settings', so we merge that stuff with the existing gist view state
|
||||
// and the default settings to make sure all necessary keys are accounted for,
|
||||
// but we're not keeping stuff that was supposed to be cleared
|
||||
const gistFromDefaults = { _state: gist._state }
|
||||
for (const d in defaultGist) {
|
||||
gistFromDefaults[d] = gist[d] === undefined ? defaultGist[d] : gist[d]
|
||||
}
|
||||
|
||||
// merge it all up
|
||||
const gistToCheck = {
|
||||
...gistFromDefaults,
|
||||
...(editedAsJson.settings ? editedAsJson.settings : editedAsJson),
|
||||
}
|
||||
|
||||
// validate it
|
||||
const validation = validateGist(gistToCheck, props.design)
|
||||
// if it's not valid, show a warning about errors
|
||||
if (!validation.valid) {
|
||||
const newError = JSON.stringify(validation.errors, null, 2)
|
||||
setError(newError)
|
||||
}
|
||||
|
||||
// save regardless
|
||||
setGist(gistToCheck)
|
||||
setSuccess(true)
|
||||
} catch (e) {
|
||||
setError(e)
|
||||
}
|
||||
}
|
||||
|
||||
const designName = capitalize(props.design.designConfig.data.name.replace('@freesewing/', ''))
|
||||
return (
|
||||
<div className="max-w-screen-xl m-auto h-screen form-control">
|
||||
<h2>{t('yamlEditViewTitleThing', { thing: designName })}</h2>
|
||||
|
||||
{error ? (
|
||||
<Popout warning className="mb-4">
|
||||
<h3> {t('yamlEditViewError')} </h3>
|
||||
{success ? <p> {t('yamlEditViewErrorDesc')}: </p> : null}
|
||||
<pre
|
||||
className="language-json hljs text-base lg:text-lg whitespace-pre overflow-scroll pr-4"
|
||||
dangerouslySetInnerHTML={{ __html: error }}
|
||||
></pre>
|
||||
</Popout>
|
||||
) : null}
|
||||
{success ? (
|
||||
<div className="alert alert-success my-4">
|
||||
<div>
|
||||
<span>{t('success')}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div id="editor" className="h-3/5 my-8">
|
||||
<textarea
|
||||
className="textarea textarea-primary w-full p-4 leading-7 text-lg h-full"
|
||||
name="gistAsYaml"
|
||||
aria-label="Configuration in YAML format"
|
||||
ref={inputRef}
|
||||
defaultValue={gistAsYaml}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={onSave}>
|
||||
{' '}
|
||||
Save{' '}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
129
sites/shared/components/workbench/en.yaml
Normal file
129
sites/shared/components/workbench/en.yaml
Normal file
|
@ -0,0 +1,129 @@
|
|||
addNotes: Add notes
|
||||
addSettingsToNotes: Add settings to notes
|
||||
advanced: Advanced
|
||||
appliedMeasies: We applied a new measurements set to this pattern.
|
||||
armhole: Armhole
|
||||
attributes: Attributes
|
||||
backPockets: Back pockets
|
||||
bookmarkPattern: Bookmark pattern
|
||||
bottomRight: Bottom Right
|
||||
changeMeasies: Change Pattern Measurements
|
||||
chooseATest: Choose a test scenario to run
|
||||
chooseATestDesc: You can test how the design adapts to changes in a specific design option or measurements.
|
||||
chooseATestMenuMobileMsg: To do so, open the test menu and select the scenario you want to run.
|
||||
chooseATestMenuMsg: To do so, select the test scenario you want to run in the menu on the right.
|
||||
chooseFromBookmarkedSets: Choose one of the measurements sets you've bookmarked
|
||||
chooseFromBookmarkedSetsDesc: If you've bookmarked any measurements sets, you can select from those too.
|
||||
chooseFromCuratedSets: Choose one of FreeSewing's curated measurements sets
|
||||
chooseFromCuratedSetsDesc: If you're just looking to try out our platform, you can select from our list of curated measurements sets.
|
||||
chooseFromOwnSets: Choose one of your own measurements sets
|
||||
chooseFromOwnSetsDesc: Pick any of your own measurements sets that have all required measurements to generate this pattern.
|
||||
chooseNewSet: Choose a New Measurements Set
|
||||
closure: Closure
|
||||
collar: Collar
|
||||
columns: columns
|
||||
configurePattern: Configure pattern
|
||||
construction: Construction
|
||||
continueEditingTitle: Continue editing
|
||||
continueEditingDesc: Load the newly saved pattern in the pattern editor, so you can make more changes.
|
||||
cuffs: Cuffs
|
||||
currentPrintLayout: Current print layout
|
||||
cutLayout: Cut Layout
|
||||
darts: Darts
|
||||
designOptions.d: Test the effect of an option on the way this pattern looks
|
||||
docs: Documentation
|
||||
draft: Draft
|
||||
draftPattern: Draft pattern
|
||||
edit: Edit
|
||||
editCurrentMeasies: Edit Current Measurements
|
||||
editCurrentMeasiesDesc: Changes you make here will not be saved to your measurements sets, and will only affect this pattern.
|
||||
editCurrentMeasiesHeader: Edit Pattern Measurements
|
||||
editMeasiesByHand: Edit measurements by hand
|
||||
editMeasiesByHandDesc: Manually set or override any measurements. These changes will only apply to the current pattern.
|
||||
editSettings: Edit configuration
|
||||
elastic: Elastic
|
||||
export: Export
|
||||
exportAsData: Export as data
|
||||
exportForEditing: Export for editing
|
||||
exportForPrinting: Export for printing
|
||||
exportPattern-txt: Export a PDF suitable for your printer, or download this pattern in a variety of formats
|
||||
exportPattern: Export pattern
|
||||
fit: Fit
|
||||
frontPockets: Front pockets
|
||||
generatePdf: Generate print-ready PDF
|
||||
giveItAName: Give it a name
|
||||
goToPatternTitle: Navigate to the pattern page
|
||||
goToPatternDesc: Exit the pattern editor and navigate to the pattern page where you can update the pattern's metadata.
|
||||
height: Height
|
||||
help: Help
|
||||
layoutSettings.d: Additional options to further optimize the printing layout of your pattern.
|
||||
layoutSettings.t: Layout settings
|
||||
length: Length
|
||||
learnHowToUseEditor: Learn how to use FreeSewing's online pattern editor
|
||||
measies: Pattern Measurements
|
||||
measiesOk: We have all required measurements to create this pattern.
|
||||
measurements.d: Test the effect of a measurement on the way this pattern looks
|
||||
menu: Menu
|
||||
name: Name
|
||||
noDesignFound: We could not find this design. This shouldn't happen, so we'd really appreciate you reporting it.
|
||||
noInlineDocs: Documention is not included in this build
|
||||
noInlineDocsDesc: Please visit FreeSewing.org to access our documentation
|
||||
notes: Notes
|
||||
pages: pages
|
||||
part: Pattern part
|
||||
partInfo: Pattern part info
|
||||
partTransfo: Part transformation buttons
|
||||
partTransfoDesc: These buttons allow you to rotate or flip individual pattern parts.
|
||||
partTransfoNo: Hide buttons
|
||||
partTransfoNoDesc: Do not include these buttons on the pattern output
|
||||
partTransfoYes: Show buttons
|
||||
partTransfoYesDesc: Include these buttons on the pattern output (they will not be printed)
|
||||
pathInfo: Path info
|
||||
patternBookmarkCreated: Pattern bookmark created
|
||||
patternInspector: Pattern Inspector
|
||||
patternLogs: Pattern logs
|
||||
patternSaved: Pattern saved
|
||||
pockets: Pockets
|
||||
printLayout: Print Layout
|
||||
printSettings.d: Configure your pattern so you can print it just the way you like it. Includes page size & orientation, margins, and more.
|
||||
printSettings.t: Print settings
|
||||
reset: Reset
|
||||
resetPrintLayout: Reset print layout
|
||||
resetPrintLayoutDesc: Removes all manual changes to the print layout, and restores the default layout
|
||||
rows: rows
|
||||
save: Save
|
||||
savePattern: Save pattern
|
||||
saveAsNewPattern: Save as new pattern
|
||||
savePatternAs: Save pattern as...
|
||||
savePatternAsHellip: Save pattern as...
|
||||
saveSettings: Save Settings
|
||||
saveYourPattern: Save your pattern
|
||||
seeMissingMeasies: See missing measurements
|
||||
show: Show
|
||||
showAllParts: Show all pattern parts
|
||||
showMovableButtons: Buttons
|
||||
showOnlyThisPart: Show only this pattern part
|
||||
sleevecap: Sleevecap
|
||||
style: Style
|
||||
test: Test
|
||||
testDesignMeasurement: "Test {design} measurements: {measurement}"
|
||||
testDesignOption: "Test {design} design options: {option}"
|
||||
testDesignSets: "Test {design} across measurements sets"
|
||||
testMeasurements: Test measurements
|
||||
testMeasurementsDesc: Test how the design adapts to changes to a specific measurement
|
||||
testOptions: Test design options
|
||||
testOptionsDesc: Test how the design adapts to changes in a specific design option
|
||||
testPattern: Test pattern
|
||||
testSets: Test measurements sets
|
||||
testSetsDesc: Test how the design adapts across different measurements sets
|
||||
topLeft: Top Left
|
||||
weLackSomeMeasies: We lack { nr } measurements to create this pattern
|
||||
whereToGoAfterSaveAs: After saving the new pattern, what do you want to do?
|
||||
width: Width
|
||||
xTotalPagesSomeBlank: "{total} pages in total but {blank} are blank"
|
||||
yamlEditViewError: Issues with YAML
|
||||
yamlEditViewErrorDesc: We saved your input, but it might not work for the following reasons
|
||||
yamlEditViewTitleThing: 'Edit Pattern Configuration for {thing}'
|
||||
youCanPickOrEnter: You can either pick a measurements set, or enter them by hand, but we cannot proceed without these measurements.
|
||||
youUseCustomValue: You are using the default value
|
||||
youUseDefaultValue: You are using a custom value
|
129
sites/shared/components/workbench/es.yaml
Normal file
129
sites/shared/components/workbench/es.yaml
Normal file
|
@ -0,0 +1,129 @@
|
|||
addNotes: Add notes
|
||||
addSettingsToNotes: Add settings to notes
|
||||
advanced: Avanzado
|
||||
appliedMeasies: We applied a new measurements set to this pattern.
|
||||
armhole: Agujero armado
|
||||
attributes: Atributos
|
||||
backPockets: Paquetes de retroceso
|
||||
bookmarkPattern: Bookmark pattern
|
||||
bottomRight: Abajo a la derecha
|
||||
changeMeasies: Mediciones del patrón de cambio
|
||||
chooseATest: Choose a test scenario to run
|
||||
chooseATestDesc: You can test how the design adapts to changes in a specific design option or measurements.
|
||||
chooseATestMenuMobileMsg: To do so, open the test menu and select the scenario you want to run.
|
||||
chooseATestMenuMsg: To do so, select the test scenario you want to run in the menu on the right.
|
||||
chooseFromBookmarkedSets: Choose one of the measurements sets you've bookmarked
|
||||
chooseFromBookmarkedSetsDesc: If you've bookmarked any measurements sets, you can select from those too.
|
||||
chooseFromCuratedSets: Choose one of FreeSewing's curated measurements sets
|
||||
chooseFromCuratedSetsDesc: If you're just looking to try out our platform, you can select from our list of curated measurements sets.
|
||||
chooseFromOwnSets: Choose one of your own measurements sets
|
||||
chooseFromOwnSetsDesc: Pick any of your own measurements sets that have all required measurements to generate this pattern.
|
||||
chooseNewSet: Elegir un nuevo conjunto de medidas
|
||||
closure: Cierre
|
||||
collar: Cuello
|
||||
columns: columns
|
||||
configurePattern: Configurar patrón
|
||||
construction: Construcción
|
||||
continueEditingTitle: Continue editing
|
||||
continueEditingDesc: Load the newly saved pattern in the pattern editor, so you can make more changes.
|
||||
cuffs: Puños
|
||||
currentPrintLayout: Current print layout
|
||||
cutLayout: Disposición del corte
|
||||
darts: Dardos
|
||||
designOptions.d: Prueba el efecto de una opción sobre el aspecto de este patrón
|
||||
docs: Documentación
|
||||
draft: Boceto
|
||||
draftPattern: Proyecto de patrón
|
||||
edit: Edita
|
||||
editCurrentMeasies: Editar medidas actuales
|
||||
editCurrentMeasiesDesc: Changes you make here will not be saved to your measurements sets, and will only affect this pattern.
|
||||
editCurrentMeasiesHeader: Edit Pattern Measurements
|
||||
editMeasiesByHand: Edit measurements by hand
|
||||
editMeasiesByHandDesc: Manually set or override any measurements. These changes will only apply to the current pattern.
|
||||
editSettings: Editar configuración
|
||||
elastic: Elástico
|
||||
export: Exportar
|
||||
exportAsData: Exportar como datos
|
||||
exportForEditing: Exportar para editar
|
||||
exportForPrinting: Exportar para impresión
|
||||
exportPattern-txt: Exporte un PDF adecuado para su impresora, o descargue este patrón en una variedad de formatos
|
||||
exportPattern: Exportar patrón
|
||||
fit: Ajuste
|
||||
frontPockets: Pockets delanteros
|
||||
generatePdf: Generate print-ready PDF
|
||||
giveItAName: Dale un nombre
|
||||
goToPatternTitle: Navigate to the pattern page
|
||||
goToPatternDesc: Exit the pattern editor and navigate to the pattern page where you can update the pattern's metadata.
|
||||
height: Altura
|
||||
help: Ayuda
|
||||
layoutSettings.d: Additional options to further optimize the printing layout of your pattern.
|
||||
layoutSettings.t: Layout settings
|
||||
length: Longitud
|
||||
learnHowToUseEditor: Learn how to use FreeSewing's online pattern editor
|
||||
measies: Medidas del patrón
|
||||
measiesOk: Tenemos todas las medidas necesarias para crear este patrón.
|
||||
measurements.d: Prueba el efecto de una medida en el aspecto de este patrón
|
||||
menu: Menú
|
||||
name: Nombre
|
||||
noDesignFound: No hemos podido encontrar este diseño. Esto no debería ocurrir, así que te agradeceríamos que nos lo comunicaras.
|
||||
noInlineDocs: Documention is not included in this build
|
||||
noInlineDocsDesc: Please visit FreeSewing.org to access our documentation
|
||||
notes: Notas
|
||||
pages: pages
|
||||
part: Parte del patrón
|
||||
partInfo: Información sobre el patrón
|
||||
partTransfo: Part transformation buttons
|
||||
partTransfoDesc: These buttons allow you to rotate or flip individual pattern parts.
|
||||
partTransfoNo: Hide buttons
|
||||
partTransfoNoDesc: Do not include these buttons on the pattern output
|
||||
partTransfoYes: Show buttons
|
||||
partTransfoYesDesc: Include these buttons on the pattern output (they will not be printed)
|
||||
pathInfo: Información de la ruta
|
||||
patternBookmarkCreated: Pattern bookmark created
|
||||
patternInspector: Inspector de Patrones
|
||||
patternLogs: Registros de patrones
|
||||
patternSaved: Pattern saved
|
||||
pockets: Bolsillos
|
||||
printLayout: Diseño de impresión
|
||||
printSettings.d: Configure your pattern so you can print it just the way you like it. Includes page size & orientation, margins, and more.
|
||||
printSettings.t: Print settings
|
||||
reset: Reiniciar
|
||||
resetPrintLayout: Reset print layout
|
||||
resetPrintLayoutDesc: Removes all manual changes to the print layout, and restores the default layout
|
||||
rows: rows
|
||||
save: Guardar
|
||||
savePattern: Guardar patrón
|
||||
saveAsNewPattern: Save as new pattern
|
||||
savePatternAs: Save pattern as...
|
||||
savePatternAsHellip: Save pattern as...
|
||||
saveSettings: Guardar ajustes
|
||||
saveYourPattern: Guarda tu patrón
|
||||
seeMissingMeasies: See missing measurements
|
||||
show: Mostrar
|
||||
showAllParts: Mostrar todas las partes del patrón
|
||||
showMovableButtons: Botones
|
||||
showOnlyThisPart: Mostrar sólo esta parte del patrón
|
||||
sleevecap: Manga corta
|
||||
style: Estilo
|
||||
test: Prueba
|
||||
testDesignMeasurement: "Test {design} measurements: {measurement}"
|
||||
testDesignOption: "Test {design} design options: {option}"
|
||||
testDesignSets: "Test {design} across measurements sets"
|
||||
testMeasurements: Test measurements
|
||||
testMeasurementsDesc: Test how the design adapts to changes to a specific measurement
|
||||
testOptions: Test design options
|
||||
testOptionsDesc: Test how the design adapts to changes in a specific design option
|
||||
testPattern: Patrón de prueba
|
||||
testSets: Test measurements sets
|
||||
testSetsDesc: Test how the design adapts across different measurements sets
|
||||
topLeft: Arriba a la izquierda
|
||||
weLackSomeMeasies: Nos faltan las medidas de { nr } para crear este patrón
|
||||
whereToGoAfterSaveAs: After saving the new pattern, what do you want to do?
|
||||
width: Anchura
|
||||
xTotalPagesSomeBlank: "{total} pages in total but {blank} are blank"
|
||||
yamlEditViewError: Problemas con YAML
|
||||
yamlEditViewErrorDesc: Hemos guardado tu entrada, pero puede que no funcione por las siguientes razones
|
||||
yamlEditViewTitleThing: 'Editar configuración de patrones para {thing}'
|
||||
youCanPickOrEnter: Puedes elegir un conjunto de medidas o introducirlas a mano, pero no podemos proceder sin estas medidas.
|
||||
youUseCustomValue: You are using the default value
|
||||
youUseDefaultValue: You are using a custom value
|
|
@ -1,45 +1,41 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import Worker from 'web-worker'
|
||||
import fileSaver from 'file-saver'
|
||||
import { themePlugin } from '@freesewing/plugin-theme'
|
||||
import { pluginI18n } from '@freesewing/plugin-i18n'
|
||||
import { pagesPlugin, fabricPlugin } from '../layout/plugin-layout-part.mjs'
|
||||
import { pluginAnnotations } from '@freesewing/plugin-annotations'
|
||||
import { cutLayoutPlugin } from '../layout/cut/plugin-cut-layout.mjs'
|
||||
import { fabricSettingsOrDefault } from '../layout/cut/index.mjs'
|
||||
import { useFabricLength } from '../layout/cut/settings.mjs'
|
||||
import { pagesPlugin, materialPlugin } from 'shared/plugins/plugin-layout-part.mjs'
|
||||
import { cutLayoutPlugin } from 'shared/plugins/plugin-cut-layout.mjs'
|
||||
import { materialSettingsOrDefault } from 'shared/components/workbench/views/cut/hooks.mjs'
|
||||
import { useMaterialLength } from 'shared/components/workbench/views/cut/hooks.mjs'
|
||||
import { capitalize, formatMm } from 'shared/utils.mjs'
|
||||
import {
|
||||
defaultPrintSettings,
|
||||
printSettingsPath,
|
||||
} from 'shared/components/workbench/views/print/config.mjs'
|
||||
import get from 'lodash.get'
|
||||
|
||||
export const ns = ['cut', 'plugin', 'common']
|
||||
export const exportTypes = {
|
||||
exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'],
|
||||
exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'legal', 'tabloid'],
|
||||
exportForEditing: ['svg', 'pdf'],
|
||||
exportAsData: ['json', 'yaml', 'github gist'],
|
||||
}
|
||||
|
||||
export const defaultPdfSettings = {
|
||||
size: 'a4',
|
||||
orientation: 'portrait',
|
||||
margin: 10,
|
||||
coverPage: true,
|
||||
cutlist: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a pattern that uses plugins theme, i18n, and cutlist
|
||||
* @param {Design} design the design to construct the pattern from
|
||||
* @param {Object} gist the gist
|
||||
* @param {Object} overwrite settings to overwrite gist settings with
|
||||
* @param {Design} Design the design to construct the pattern from
|
||||
* @param {Object} settings the settings
|
||||
* @param {Object} overwrite settings to overwrite settings settings with
|
||||
* @param {string} format the export format this pattern will be prepared for
|
||||
* @param {function} t the i18n function
|
||||
* @return {Pattern} a pattern
|
||||
*/
|
||||
const themedPattern = (design, gist, overwrite, format, t) => {
|
||||
const pattern = new design({ ...gist, ...overwrite })
|
||||
const themedPattern = (Design, settings, overwrite, format, t) => {
|
||||
const pattern = new Design({ ...settings, ...overwrite })
|
||||
|
||||
// add the theme and translation to the pattern
|
||||
pattern.use(themePlugin, { stripped: format !== 'svg', skipGrid: ['pages'] })
|
||||
pattern.use(pluginI18n, { t })
|
||||
pattern.use(pluginAnnotations)
|
||||
|
||||
return pattern
|
||||
}
|
||||
|
@ -48,42 +44,42 @@ const themedPattern = (design, gist, overwrite, format, t) => {
|
|||
* Generate svgs of all cutting layouts for the pattern
|
||||
* @param {Pattern} pattern the pattern to generate cutting layouts for
|
||||
* @param {Design} design the design constructor for the pattern
|
||||
* @param {Object} gist the gist
|
||||
* @param {Object} settings the settings
|
||||
* @param {string} format the export format this pattern will be prepared for
|
||||
* @param {function} t the i18n function
|
||||
* @return {Object} a dictionary of svgs and related translation strings, keyed by fabric
|
||||
* @return {Object} a dictionary of svgs and related translation strings, keyed by material
|
||||
*/
|
||||
const generateCutLayouts = (pattern, design, gist, format, t) => {
|
||||
// get the fabrics from the already drafted base pattern
|
||||
const fabrics = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics(
|
||||
const generateCutLayouts = (pattern, Design, settings, format, t, ui) => {
|
||||
// get the materials from the already drafted base pattern
|
||||
const materials = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics(
|
||||
pattern.settings[0]
|
||||
) || ['fabric']
|
||||
if (!fabrics.length) return
|
||||
if (!materials.length) return
|
||||
|
||||
const isImperial = gist.units === 'imperial'
|
||||
const isImperial = settings.units === 'imperial'
|
||||
const cutLayouts = {}
|
||||
// each fabric
|
||||
fabrics.forEach((f) => {
|
||||
// get the settings and layout for that fabric
|
||||
const fabricSettings = fabricSettingsOrDefault(gist, f)
|
||||
const fabricLayout = get(gist, ['layouts', 'cuttingLayout', f], true)
|
||||
// each material
|
||||
materials.forEach((f) => {
|
||||
// get the settings and layout for that material
|
||||
const materialSettings = materialSettingsOrDefault(settings.units, ui, f)
|
||||
const materialLayout = get(ui, ['layouts', 'cut', f], true)
|
||||
|
||||
// make a new pattern
|
||||
const fabricPattern = themedPattern(design, gist, { layout: fabricLayout }, format, t)
|
||||
// add cut layout plugin and fabric plugin
|
||||
.use(cutLayoutPlugin(f, fabricSettings.grainDirection))
|
||||
.use(fabricPlugin({ ...fabricSettings, printStyle: true, setPatternSize: 'width' }))
|
||||
const materialPattern = themedPattern(Design, settings, { layout: materialLayout }, format, t)
|
||||
// add cut layout plugin and material plugin
|
||||
.use(cutLayoutPlugin(f, materialSettings.grainDirection))
|
||||
.use(materialPlugin({ ...materialSettings, printStyle: true, setPatternSize: 'width' }))
|
||||
|
||||
// draft and render
|
||||
fabricPattern.draft()
|
||||
const svg = fabricPattern.render()
|
||||
materialPattern.draft()
|
||||
const svg = materialPattern.render()
|
||||
// include translations
|
||||
cutLayouts[f] = {
|
||||
svg,
|
||||
title: t('plugin:' + f),
|
||||
dimensions: t('plugin:fabricSize', {
|
||||
width: formatMm(fabricSettings.sheetWidth, gist.units, 'notags'),
|
||||
length: useFabricLength(isImperial, fabricPattern.height, 'notags'),
|
||||
title: t('cut:' + f),
|
||||
dimensions: t('cut:materialSize', {
|
||||
width: formatMm(materialSettings.sheetWidth, settings.units, 'notags'),
|
||||
length: useMaterialLength(isImperial, materialPattern.height, 'notags'),
|
||||
interpolation: { escapeValue: false },
|
||||
}),
|
||||
}
|
||||
|
@ -92,18 +88,28 @@ const generateCutLayouts = (pattern, design, gist, format, t) => {
|
|||
return cutLayouts
|
||||
}
|
||||
/**
|
||||
* Handle exporting the draft or gist
|
||||
* Handle exporting the draft or settings
|
||||
* format: format to export to
|
||||
* gist: the gist
|
||||
* design: the pattern constructor for the design to be exported
|
||||
* settings: the settings
|
||||
* Design: the pattern constructor for the design to be exported
|
||||
* t: a translation function to attach to the draft
|
||||
* app: an app instance
|
||||
* onComplete: business to perform after a successful export
|
||||
* onError: business to perform on error
|
||||
* */
|
||||
export const handleExport = async (format, gist, design, t, app, onComplete, onError) => {
|
||||
export const handleExport = async ({
|
||||
format,
|
||||
settings,
|
||||
Design,
|
||||
design,
|
||||
t,
|
||||
startLoading,
|
||||
stopLoading,
|
||||
onComplete,
|
||||
onError,
|
||||
ui,
|
||||
}) => {
|
||||
// start the loading indicator
|
||||
app.startLoading()
|
||||
if (typeof startLoading === 'function') startLoading()
|
||||
|
||||
// get a worker going
|
||||
const worker = new Worker(new URL('./export-worker.js', import.meta.url), { type: 'module' })
|
||||
|
@ -115,7 +121,7 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
|
|||
// save it out
|
||||
if (e.data.blob) {
|
||||
const fileType = exportTypes.exportForPrinting.indexOf(format) === -1 ? format : 'pdf'
|
||||
fileSaver.saveAs(e.data.blob, `freesewing-${gist.design || 'gist'}.${fileType}`)
|
||||
fileSaver.saveAs(e.data.blob, `freesewing-${design || 'pattern'}.${fileType}`)
|
||||
}
|
||||
// do additional business
|
||||
onComplete && onComplete(e)
|
||||
|
@ -127,37 +133,35 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
|
|||
}
|
||||
|
||||
// stop the loader
|
||||
app.stopLoading()
|
||||
if (typeof stopLoading === 'function') stopLoading()
|
||||
})
|
||||
|
||||
// pdf settings
|
||||
const settings = {
|
||||
...defaultPdfSettings,
|
||||
...(gist._state.layout?.forPrinting?.page || {}),
|
||||
const pageSettings = {
|
||||
...defaultPrintSettings(settings.units),
|
||||
...get(ui, printSettingsPath, {}),
|
||||
}
|
||||
|
||||
// arguments to pass to the worker
|
||||
const workerArgs = { format, gist, settings }
|
||||
const workerArgs = { format, settings, pageSettings }
|
||||
|
||||
// data passed to the worker must be JSON serializable, so we can't pass functions or prototypes
|
||||
// that means if it's not a data export there's more work to do before we can hand off to the worker
|
||||
if (exportTypes.exportAsData.indexOf(format) === -1) {
|
||||
gist.embed = false
|
||||
settings.embed = false
|
||||
// make a pattern instance for export rendering
|
||||
const layout = gist.layouts?.printingLayout || gist.layout || true
|
||||
let pattern = themedPattern(design, gist, { layout }, format, t)
|
||||
const layout = settings.layout || ui.layouts?.print || true
|
||||
let pattern = themedPattern(Design, settings, { layout }, format, t)
|
||||
|
||||
// a specified size should override the gist one
|
||||
if (format !== 'pdf') {
|
||||
settings.size = format
|
||||
}
|
||||
// a specified size should override the settings one
|
||||
pageSettings.size = format
|
||||
|
||||
try {
|
||||
// add pages to pdf exports
|
||||
if (format !== 'svg') {
|
||||
if (!exportTypes.exportForEditing.includes(format)) {
|
||||
pattern.use(
|
||||
pagesPlugin({
|
||||
...settings,
|
||||
...pageSettings,
|
||||
printStyle: true,
|
||||
renderBlanks: false,
|
||||
setPatternSize: true,
|
||||
|
@ -166,10 +170,10 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
|
|||
|
||||
// add the strings that are used on the cover page
|
||||
workerArgs.strings = {
|
||||
design: capitalize(pattern.designConfig.data.name.replace('@freesewing/', '')),
|
||||
design: capitalize(design),
|
||||
tagline: t('common:sloganCome') + '. ' + t('common:sloganStay'),
|
||||
url: window.location.href,
|
||||
cuttingLayout: t('plugin:cuttingLayout'),
|
||||
cuttingLayout: t('cut:cuttingLayout'),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,20 +181,22 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
|
|||
pattern.draft()
|
||||
workerArgs.svg = pattern.render()
|
||||
|
||||
if (format === 'pdf') pageSettings.size = [pattern.width, pattern.height]
|
||||
|
||||
// add the svg and pages data to the worker args
|
||||
workerArgs.pages = pattern.setStores[pattern.activeSet].get('pages')
|
||||
|
||||
// add cutting layouts if requested
|
||||
if (format !== 'svg' && settings.cutlist) {
|
||||
workerArgs.cutLayouts = generateCutLayouts(pattern, design, gist, format, t)
|
||||
if (!exportTypes.exportForEditing.includes(format) && pageSettings.cutlist) {
|
||||
workerArgs.cutLayouts = generateCutLayouts(pattern, Design, settings, format, t, ui)
|
||||
}
|
||||
|
||||
// post a message to the worker with all needed data
|
||||
worker.postMessage(workerArgs)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
app.stopLoading()
|
||||
if (typeof stopLoading === 'function') stopLoading()
|
||||
onError && onError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// post a message to the worker with all needed data
|
||||
worker.postMessage(workerArgs)
|
||||
}
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
/**
|
||||
* A web worker to handle the business of exporting pattern files
|
||||
* */
|
||||
import yaml from 'js-yaml'
|
||||
import axios from 'axios'
|
||||
import { PdfMaker } from './pdf-maker'
|
||||
import { SinglePdfMaker } from './single-pdf-maker.mjs'
|
||||
|
||||
/** when the worker receives data from the page, do the appropriate export */
|
||||
addEventListener('message', async (e) => {
|
||||
const { format, gist, svg } = e.data
|
||||
const { format, settings, svg } = e.data
|
||||
// handle export by type
|
||||
try {
|
||||
if (format === 'json') return exportJson(gist)
|
||||
if (format === 'yaml') return exportYaml(gist)
|
||||
if (format === 'github gist') return exportGithubGist(gist)
|
||||
|
||||
if (format === 'svg') return exportSvg(svg)
|
||||
|
||||
await exportPdf(e.data)
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return exportJson(settings)
|
||||
case 'yaml':
|
||||
return exportYaml(settings)
|
||||
case 'github gist':
|
||||
return exportGithubGist(settings)
|
||||
case 'svg':
|
||||
return exportSvg(svg)
|
||||
default:
|
||||
return await exportPdf(e.data)
|
||||
}
|
||||
} catch (e) {
|
||||
postMessage({ success: false, error: e })
|
||||
close()
|
||||
|
@ -37,14 +44,14 @@ const exportBlob = (blobContent, type) => {
|
|||
postSuccess(blob)
|
||||
}
|
||||
|
||||
const exportJson = (gist) => exportBlob(JSON.stringify(gist, null, 2), 'application/json')
|
||||
const exportJson = (settings) => exportBlob(JSON.stringify(settings, null, 2), 'application/json')
|
||||
|
||||
const exportYaml = (gist) => exportBlob(yaml.dump(gist), 'application/x-yaml')
|
||||
const exportYaml = (settings) => exportBlob(yaml.dump(settings), 'application/x-yaml')
|
||||
|
||||
const exportSvg = (svg) => exportBlob(svg, 'image/svg+xml')
|
||||
|
||||
const exportPdf = async (data) => {
|
||||
const maker = new PdfMaker(data)
|
||||
const maker = data.format === 'pdf' ? new SinglePdfMaker(data) : new PdfMaker(data)
|
||||
await maker.makePdf()
|
||||
postSuccess(await maker.toBlob())
|
||||
}
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { Popout } from 'shared/components/popout.mjs'
|
||||
import { WebLink } from 'shared/components/web-link.mjs'
|
||||
import { exportTypes, handleExport } from './export-handler.mjs'
|
||||
|
||||
export const ExportDraft = ({ gist, design, app }) => {
|
||||
const [link, setLink] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [format, setFormat] = useState(false)
|
||||
|
||||
const { t } = useTranslation(['app', 'plugin'])
|
||||
const doExport = (format) => {
|
||||
setLink(false)
|
||||
setError(false)
|
||||
setFormat(format)
|
||||
handleExport(
|
||||
format,
|
||||
gist,
|
||||
design,
|
||||
t,
|
||||
app,
|
||||
(e) => {
|
||||
if (e.data.link) {
|
||||
setLink(e.data.link)
|
||||
}
|
||||
},
|
||||
(e) => {
|
||||
if (e.data?.error) {
|
||||
setError(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-screen-xl m-auto">
|
||||
<h2>{t('export')}</h2>
|
||||
<p className="text-lg sm:text-xl">{t('exportPattern-txt')}</p>
|
||||
{link && (
|
||||
<Popout link compact>
|
||||
<span className="font-bold mr-4 uppercase text-sm">{format}:</span>
|
||||
<WebLink href={link} txt={link} />
|
||||
</Popout>
|
||||
)}
|
||||
{error && (
|
||||
<Popout warning compact>
|
||||
<span className="font-bold mr-4 uppercase text-sm">{t('error')}:</span>
|
||||
{t('somethingWentWrong')}
|
||||
</Popout>
|
||||
)}
|
||||
<div className="flex flex-row flex-wrap gap-8">
|
||||
{Object.keys(exportTypes).map((type) => (
|
||||
<div key={type} className="flex flex-col gap-2 w-full sm:w-auto">
|
||||
<h4>{t(type)}</h4>
|
||||
{exportTypes[type].map((format) => (
|
||||
<button key={format} className="btn btn-primary" onClick={() => doExport(format)}>
|
||||
{type === 'exportForPrinting' ? `${format} pdf` : format}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import PDFDocument from 'pdfkit/js/pdfkit.standalone'
|
||||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { Pdf, mmToPoints } from './pdf.mjs'
|
||||
import SVGtoPDF from 'svg-to-pdfkit'
|
||||
import { logoPath } from 'shared/components/logos/freesewing.mjs'
|
||||
|
||||
|
@ -8,11 +9,6 @@ const logoSvg = `<svg viewBox="0 0 25 25">
|
|||
<path d="${logoPath}" />
|
||||
</svg>`
|
||||
|
||||
/**
|
||||
* PdfKit, the library we're using for pdf generation, uses points as a unit, so when we tell it things like where to put the svg and how big the svg is, we need those numbers to be in points
|
||||
* The svg uses mm internally, so when we do spatial reasoning inside the svg, we need to know values in mm
|
||||
* */
|
||||
const mmToPoints = 2.834645669291339
|
||||
const lineStart = 50
|
||||
/**
|
||||
* Freesewing's first explicit class?
|
||||
|
@ -22,7 +18,7 @@ export class PdfMaker {
|
|||
/** the svg as text to embed in the pdf */
|
||||
svg
|
||||
/** the document configuration */
|
||||
settings
|
||||
pageSettings
|
||||
/** the pdfKit instance that is writing the document */
|
||||
pdf
|
||||
/** the export buffer to hold pdfKit output */
|
||||
|
@ -51,16 +47,19 @@ export class PdfMaker {
|
|||
pageCount = 0
|
||||
lineLevel = 50
|
||||
|
||||
constructor({ svg, settings, pages, strings, cutLayouts }) {
|
||||
this.settings = settings
|
||||
constructor({ svg, pageSettings, pages, strings, cutLayouts }) {
|
||||
this.pageSettings = pageSettings
|
||||
this.pagesWithContent = pages.withContent
|
||||
this.svg = svg
|
||||
this.strings = strings
|
||||
this.cutLayouts = cutLayouts
|
||||
|
||||
this.initPdf()
|
||||
this.pdf = Pdf({
|
||||
size: this.pageSettings.size.toUpperCase(),
|
||||
layout: this.pageSettings.orientation,
|
||||
})
|
||||
|
||||
this.margin = this.settings.margin * mmToPoints // margin is in mm because it comes from us, so we convert it to points
|
||||
this.margin = this.pageSettings.margin * mmToPoints // margin is in mm because it comes from us, so we convert it to points
|
||||
this.pageHeight = this.pdf.page.height - this.margin * 2 // this is in points because it comes from pdfKit
|
||||
this.pageWidth = this.pdf.page.width - this.margin * 2 // this is in points because it comes from pdfKit
|
||||
|
||||
|
@ -73,22 +72,6 @@ export class PdfMaker {
|
|||
this.svgHeight = this.rows * this.pageHeight
|
||||
}
|
||||
|
||||
/** create the pdf document */
|
||||
initPdf() {
|
||||
// instantiate with the correct size and orientation
|
||||
this.pdf = new PDFDocument({
|
||||
size: this.settings.size.toUpperCase(),
|
||||
layout: this.settings.orientation,
|
||||
})
|
||||
|
||||
// PdfKit wants to flush the buffer on each new page.
|
||||
// We can't save directly from inside a worker, so we have to manage the buffers ourselves so we can return a blob
|
||||
this.buffers = []
|
||||
|
||||
// use a listener to add new data to our buffer storage
|
||||
this.pdf.on('data', this.buffers.push.bind(this.buffers))
|
||||
}
|
||||
|
||||
/** make the pdf */
|
||||
async makePdf() {
|
||||
await this.generateCoverPage()
|
||||
|
@ -97,27 +80,14 @@ export class PdfMaker {
|
|||
}
|
||||
|
||||
/** convert the pdf to a blob */
|
||||
toBlob() {
|
||||
return new Promise((resolve) => {
|
||||
// have to do it this way so that the document flushes everything to buffers
|
||||
this.pdf.on('end', () => {
|
||||
// convert buffers to a blob
|
||||
resolve(
|
||||
new Blob(this.buffers, {
|
||||
type: 'application/pdf',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// end the stream
|
||||
this.pdf.end()
|
||||
})
|
||||
async toBlob() {
|
||||
return this.pdf.toBlob()
|
||||
}
|
||||
|
||||
/** generate the cover page for the pdf */
|
||||
async generateCoverPage() {
|
||||
// don't make one if it's not requested
|
||||
if (!this.settings.coverPage) {
|
||||
if (!this.pageSettings.coverPage) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -170,8 +140,8 @@ export class PdfMaker {
|
|||
}
|
||||
|
||||
/** generate the title for a cutting layout page */
|
||||
async generateCutLayoutTitle(fabricTitle, fabricDimensions) {
|
||||
this.addText(this.strings.cuttingLayout, 12, 2).addText(fabricTitle, 28)
|
||||
async generateCutLayoutTitle(materialTitle, materialDimensions) {
|
||||
this.addText(this.strings.cuttingLayout, 12, 2).addText(materialTitle, 28)
|
||||
|
||||
this.pdf.lineWidth(1)
|
||||
this.pdf
|
||||
|
@ -180,16 +150,16 @@ export class PdfMaker {
|
|||
.stroke()
|
||||
|
||||
this.lineLevel += 5
|
||||
this.addText(fabricDimensions, 16)
|
||||
this.addText(materialDimensions, 16)
|
||||
}
|
||||
|
||||
/** generate all cutting layout pages */
|
||||
async generateCutLayoutPages() {
|
||||
if (!this.settings.cutlist || !this.cutLayouts) return
|
||||
if (!this.pageSettings.cutlist || !this.cutLayouts) return
|
||||
|
||||
for (const fabric in this.cutLayouts) {
|
||||
for (const material in this.cutLayouts) {
|
||||
this.nextPage()
|
||||
const { title, dimensions, svg } = this.cutLayouts[fabric]
|
||||
const { title, dimensions, svg } = this.cutLayouts[material]
|
||||
await this.generateCutLayoutTitle(title, dimensions)
|
||||
await this.generateSvgPage(svg)
|
||||
}
|
||||
|
|
46
sites/shared/components/workbench/exporting/pdf.mjs
Normal file
46
sites/shared/components/workbench/exporting/pdf.mjs
Normal file
|
@ -0,0 +1,46 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone'
|
||||
|
||||
/**
|
||||
* PdfKit, the library we're using for pdf generation, uses points as a unit, so when we tell it things like where to put the svg and how big the svg is, we need those numbers to be in points
|
||||
* The svg uses mm internally, so when we do spatial reasoning inside the svg, we need to know values in mm
|
||||
* */
|
||||
export const mmToPoints = 2.834645669291339
|
||||
|
||||
/**
|
||||
* A PDFKit Pdf with a few of our utilities included
|
||||
* @returns
|
||||
*/
|
||||
export const Pdf = ({ size, layout }) => {
|
||||
const pdf = new PDFDocument({
|
||||
size,
|
||||
layout,
|
||||
})
|
||||
|
||||
// PdfKit wants to flush the buffer on each new page.
|
||||
// We can't save directly from inside a worker, so we have to manage the buffers ourselves so we can return a blob
|
||||
const buffers = []
|
||||
|
||||
// use a listener to add new data to our buffer storage
|
||||
pdf.on('data', buffers.push.bind(buffers))
|
||||
|
||||
/** convert the pdf to a blob */
|
||||
pdf.toBlob = function () {
|
||||
return new Promise((resolve) => {
|
||||
// have to do it this way so that the document flushes everything to buffers
|
||||
pdf.on('end', () => {
|
||||
// convert buffers to a blob
|
||||
resolve(
|
||||
new Blob(buffers, {
|
||||
type: 'application/pdf',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// end the stream
|
||||
pdf.end()
|
||||
})
|
||||
}
|
||||
|
||||
return pdf
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { Pdf, mmToPoints } from './pdf.mjs'
|
||||
import SVGtoPDF from 'svg-to-pdfkit'
|
||||
|
||||
/**
|
||||
* Basic exporter for a single-page pdf containing the rendered pattern.
|
||||
* This generates a PDF that is the size of the pattern and has no additional frills*/
|
||||
export class SinglePdfMaker {
|
||||
pdf
|
||||
svg
|
||||
|
||||
constructor({ svg, pageSettings }) {
|
||||
this.pdf = Pdf({ size: pageSettings.size.map((s) => s * mmToPoints) })
|
||||
this.svg = svg
|
||||
}
|
||||
|
||||
async makePdf() {
|
||||
await SVGtoPDF(this.pdf, this.svg)
|
||||
}
|
||||
|
||||
async toBlob() {
|
||||
return this.pdf.toBlob()
|
||||
}
|
||||
}
|
129
sites/shared/components/workbench/fr.yaml
Normal file
129
sites/shared/components/workbench/fr.yaml
Normal file
|
@ -0,0 +1,129 @@
|
|||
addNotes: Add notes
|
||||
addSettingsToNotes: Add settings to notes
|
||||
advanced: Avancé
|
||||
appliedMeasies: We applied a new measurements set to this pattern.
|
||||
armhole: Emmanchure
|
||||
attributes: Attributs
|
||||
backPockets: Poches arrière
|
||||
bookmarkPattern: Bookmark pattern
|
||||
bottomRight: En bas à droite
|
||||
changeMeasies: Mesures du schéma de changement
|
||||
chooseATest: Choose a test scenario to run
|
||||
chooseATestDesc: You can test how the design adapts to changes in a specific design option or measurements.
|
||||
chooseATestMenuMobileMsg: To do so, open the test menu and select the scenario you want to run.
|
||||
chooseATestMenuMsg: To do so, select the test scenario you want to run in the menu on the right.
|
||||
chooseFromBookmarkedSets: Choose one of the measurements sets you've bookmarked
|
||||
chooseFromBookmarkedSetsDesc: If you've bookmarked any measurements sets, you can select from those too.
|
||||
chooseFromCuratedSets: Choose one of FreeSewing's curated measurements sets
|
||||
chooseFromCuratedSetsDesc: If you're just looking to try out our platform, you can select from our list of curated measurements sets.
|
||||
chooseFromOwnSets: Choose one of your own measurements sets
|
||||
chooseFromOwnSetsDesc: Pick any of your own measurements sets that have all required measurements to generate this pattern.
|
||||
chooseNewSet: Choisis un nouvel ensemble de mesures
|
||||
closure: Fermeture
|
||||
collar: Col
|
||||
columns: columns
|
||||
configurePattern: Configurer le modèle
|
||||
construction: Construction
|
||||
continueEditingTitle: Continue editing
|
||||
continueEditingDesc: Load the newly saved pattern in the pattern editor, so you can make more changes.
|
||||
cuffs: Poignets
|
||||
currentPrintLayout: Current print layout
|
||||
cutLayout: Disposition des coupes
|
||||
darts: Pinces
|
||||
designOptions.d: Teste l'effet d'une option sur l'apparence de ce modèle.
|
||||
docs: Documentation
|
||||
draft: Ébauche
|
||||
draftPattern: Modèle d'ébauche
|
||||
edit: Éditer
|
||||
editCurrentMeasies: Modifier les mesures actuelles
|
||||
editCurrentMeasiesDesc: Changes you make here will not be saved to your measurements sets, and will only affect this pattern.
|
||||
editCurrentMeasiesHeader: Edit Pattern Measurements
|
||||
editMeasiesByHand: Edit measurements by hand
|
||||
editMeasiesByHandDesc: Manually set or override any measurements. These changes will only apply to the current pattern.
|
||||
editSettings: Modifier la configuration
|
||||
elastic: Élastique
|
||||
export: Exporter
|
||||
exportAsData: Exporter en tant que données
|
||||
exportForEditing: Exporter pour édition
|
||||
exportForPrinting: Exporter pour l'impression
|
||||
exportPattern-txt: Exporter au format PDF adapté à votre imprimante, ou télécharger ce modèle dans une variété de formats
|
||||
exportPattern: Exporter le patron
|
||||
fit: Ajustement
|
||||
frontPockets: Poches avant
|
||||
generatePdf: Generate print-ready PDF
|
||||
giveItAName: Donne-lui un nom
|
||||
goToPatternTitle: Navigate to the pattern page
|
||||
goToPatternDesc: Exit the pattern editor and navigate to the pattern page where you can update the pattern's metadata.
|
||||
height: Hauteur
|
||||
help: Aide
|
||||
layoutSettings.d: Additional options to further optimize the printing layout of your pattern.
|
||||
layoutSettings.t: Layout settings
|
||||
length: Longueur
|
||||
learnHowToUseEditor: Learn how to use FreeSewing's online pattern editor
|
||||
measies: Mesures du motif
|
||||
measiesOk: Nous avons toutes les mesures requises pour créer ce modèle.
|
||||
measurements.d: Teste l'effet d'une mesure sur l'apparence de ce motif.
|
||||
menu: Menu
|
||||
name: Nom
|
||||
noDesignFound: Nous n'avons pas pu trouver ce modèle. Cela ne devrait pas arriver, alors nous te serions très reconnaissants de nous le signaler.
|
||||
noInlineDocs: Documention is not included in this build
|
||||
noInlineDocsDesc: Please visit FreeSewing.org to access our documentation
|
||||
notes: Remarques
|
||||
pages: pages
|
||||
part: Partie du modèle
|
||||
partInfo: Informations sur les pièces du modèle
|
||||
partTransfo: Part transformation buttons
|
||||
partTransfoDesc: These buttons allow you to rotate or flip individual pattern parts.
|
||||
partTransfoNo: Hide buttons
|
||||
partTransfoNoDesc: Do not include these buttons on the pattern output
|
||||
partTransfoYes: Show buttons
|
||||
partTransfoYesDesc: Include these buttons on the pattern output (they will not be printed)
|
||||
pathInfo: Informations sur le chemin
|
||||
patternBookmarkCreated: Pattern bookmark created
|
||||
patternInspector: Inspecteur de modèles
|
||||
patternLogs: Fiches d'information sur les modèles
|
||||
patternSaved: Pattern saved
|
||||
pockets: Poches
|
||||
printLayout: Mise en page d'impression
|
||||
printSettings.d: Configure your pattern so you can print it just the way you like it. Includes page size & orientation, margins, and more.
|
||||
printSettings.t: Print settings
|
||||
reset: Réinitialiser
|
||||
resetPrintLayout: Reset print layout
|
||||
resetPrintLayoutDesc: Removes all manual changes to the print layout, and restores the default layout
|
||||
rows: rows
|
||||
save: Sauvegarder
|
||||
savePattern: Enregistrer le patron
|
||||
saveAsNewPattern: Save as new pattern
|
||||
savePatternAs: Save pattern as...
|
||||
savePatternAsHellip: Save pattern as...
|
||||
saveSettings: Sauvegarder les paramètres
|
||||
saveYourPattern: Sauvegarde ton modèle
|
||||
seeMissingMeasies: See missing measurements
|
||||
show: Afficher
|
||||
showAllParts: Afficher toutes les parties du modèle
|
||||
showMovableButtons: Boutons
|
||||
showOnlyThisPart: Ne montre que cette partie du modèle
|
||||
sleevecap: Tête de manche
|
||||
style: Style
|
||||
test: Test
|
||||
testDesignMeasurement: "Test {design} measurements: {measurement}"
|
||||
testDesignOption: "Test {design} design options: {option}"
|
||||
testDesignSets: "Test {design} across measurements sets"
|
||||
testMeasurements: Tester les mesures
|
||||
testMeasurementsDesc: Test how the design adapts to changes to a specific measurement
|
||||
testOptions: Test design options
|
||||
testOptionsDesc: Test how the design adapts to changes in a specific design option
|
||||
testPattern: Modèle de test
|
||||
testSets: Test measurements sets
|
||||
testSetsDesc: Test how the design adapts across different measurements sets
|
||||
topLeft: En haut à gauche
|
||||
weLackSomeMeasies: Il nous manque { nr } pour créer ce modèle
|
||||
whereToGoAfterSaveAs: After saving the new pattern, what do you want to do?
|
||||
width: Largeur
|
||||
xTotalPagesSomeBlank: "{total} pages in total but {blank} are blank"
|
||||
yamlEditViewError: Problèmes avec YAML
|
||||
yamlEditViewErrorDesc: Nous avons sauvegardé votre entrée, mais il se peut que cela ne fonctionne pas pour les raisons suivantes
|
||||
yamlEditViewTitleThing: 'Modifier la configuration du patron pour {thing}'
|
||||
youCanPickOrEnter: Tu peux soit choisir un ensemble de mesures, soit les saisir à la main, mais nous ne pouvons pas procéder sans ces mesures.
|
||||
youUseCustomValue: You are using the default value
|
||||
youUseDefaultValue: You are using a custom value
|
|
@ -27,7 +27,7 @@ export const defaultGist = {
|
|||
margin: 2,
|
||||
renderer: 'react',
|
||||
embed: true,
|
||||
debug: true,
|
||||
expand: false,
|
||||
}
|
||||
|
||||
export const preloadGist = {
|
||||
|
|
225
sites/shared/components/workbench/header.mjs
Normal file
225
sites/shared/components/workbench/header.mjs
Normal file
|
@ -0,0 +1,225 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { controlLevels } from 'shared/config/freesewing.config.mjs'
|
||||
// Hooks
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import {
|
||||
BeakerIcon,
|
||||
CodeIcon,
|
||||
CutIcon,
|
||||
OptionsIcon,
|
||||
PrintIcon,
|
||||
SaveIcon,
|
||||
SaveAsIcon,
|
||||
RightIcon,
|
||||
LeftIcon,
|
||||
DocsIcon,
|
||||
MeasieIcon,
|
||||
XrayIcon,
|
||||
EditIcon,
|
||||
ExportIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import Link from 'next/link'
|
||||
import { MenuWrapper } from 'shared/components/workbench/menus/shared/menu-wrapper.mjs'
|
||||
|
||||
export const ns = ['workbench', 'sections']
|
||||
|
||||
const icons = {
|
||||
test: BeakerIcon,
|
||||
export: ExportIcon,
|
||||
Edit: EditIcon,
|
||||
cut: CutIcon,
|
||||
draft: OptionsIcon,
|
||||
print: PrintIcon,
|
||||
save: SaveIcon,
|
||||
saveas: SaveAsIcon,
|
||||
logs: CodeIcon,
|
||||
inspect: XrayIcon,
|
||||
measies: MeasieIcon,
|
||||
}
|
||||
|
||||
export const NavButton = ({
|
||||
href,
|
||||
label,
|
||||
children,
|
||||
onClick = false,
|
||||
active = false,
|
||||
extraClasses = 'lg:hover:bg-secondary lg:hover:text-secondary-content',
|
||||
}) => {
|
||||
const className = `w-full flex flex-row items-center px-4 py-2 ${extraClasses} ${
|
||||
active
|
||||
? 'font-bold lg:font-normal bg-secondary bg-opacity-10 lg:bg-secondary lg:text-secondary-content lg:bg-opacity-50'
|
||||
: 'lg:bg-neutral lg:text-neutral-content'
|
||||
}`
|
||||
const span = <span className="block grow text-left">{label}</span>
|
||||
|
||||
return onClick ? (
|
||||
<button {...{ onClick, className }} title={label}>
|
||||
{span}
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
<Link {...{ href, className }} title={label}>
|
||||
{span}
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const NavIcons = ({ setView, setDense, dense, view, saveAs = false, control }) => {
|
||||
const { t } = useTranslation(['header'])
|
||||
const iconSize = 'h-6 w-6 grow-0'
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavButton
|
||||
onClick={() => setDense(!dense)}
|
||||
label=""
|
||||
extraClasses="hidden lg:flex text-accent bg-neutral hover:bg-accent hover:text-neutral-content"
|
||||
>
|
||||
{dense ? (
|
||||
<RightIcon
|
||||
className={`${iconSize} group-hover:animate-[bounceright_1s_infinite] animate-[bounceright_1s_5]`}
|
||||
stroke={4}
|
||||
/>
|
||||
) : (
|
||||
<LeftIcon className={`${iconSize} animate-bounce-right`} stroke={4} />
|
||||
)}
|
||||
</NavButton>
|
||||
{control >= controlLevels.views.draft && (
|
||||
<NavButton
|
||||
onClick={() => setView('draft')}
|
||||
label={t('workbench:patternEditor')}
|
||||
active={view === 'draft'}
|
||||
>
|
||||
<OptionsIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
{control >= controlLevels.views.measies && (
|
||||
<NavButton
|
||||
onClick={() => setView('measies')}
|
||||
label={t('workbench:measies')}
|
||||
active={view === 'measies'}
|
||||
>
|
||||
<MeasieIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
{control >= controlLevels.views.test && (
|
||||
<NavButton
|
||||
onClick={() => setView('test')}
|
||||
label={t('workbench:patternTests')}
|
||||
active={view === 'test'}
|
||||
>
|
||||
<BeakerIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
{control >= controlLevels.views.print && (
|
||||
<NavButton
|
||||
onClick={() => setView('print')}
|
||||
label={t('workbench:printLayout')}
|
||||
active={view === 'print'}
|
||||
>
|
||||
<PrintIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
{/*!isProduction && (
|
||||
<NavButton
|
||||
onClick={() => setView('cut')}
|
||||
label={t('workbench:cutLayout')}
|
||||
active={view === 'cut'}
|
||||
>
|
||||
<CutIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)*/}
|
||||
{control >= controlLevels.views.save && (
|
||||
<NavButton
|
||||
onClick={() => setView('save')}
|
||||
label={t(`workbench:${saveAs ? 'savePattern' : 'savePatternAsHellip'}`)}
|
||||
active={view === 'save'}
|
||||
>
|
||||
{saveAs ? <SaveIcon className={iconSize} /> : <SaveAsIcon className={iconSize} />}
|
||||
</NavButton>
|
||||
)}
|
||||
{control >= controlLevels.views.export && (
|
||||
<NavButton
|
||||
onClick={() => setView('export')}
|
||||
label={t('workbench:exportPattern')}
|
||||
active={view === 'export'}
|
||||
>
|
||||
<ExportIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
{control >= controlLevels.views.edit && (
|
||||
<NavButton
|
||||
onClick={() => setView('edit')}
|
||||
label={t('workbench:editByHand')}
|
||||
active={view === 'edit'}
|
||||
>
|
||||
<EditIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}{' '}
|
||||
{control >= controlLevels.views.logs && (
|
||||
<NavButton
|
||||
onClick={() => setView('logs')}
|
||||
label={t('workbench:patternLogs')}
|
||||
active={view === 'logs'}
|
||||
>
|
||||
<CodeIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
{control >= controlLevels.views.inspect && (
|
||||
<NavButton
|
||||
onClick={() => setView('inspect')}
|
||||
label={t('workbench:patternInspector')}
|
||||
active={view === 'inspect'}
|
||||
>
|
||||
<XrayIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
{control >= controlLevels.views.docs && (
|
||||
<NavButton
|
||||
onClick={() => setView('docs')}
|
||||
label={t('workbench:docs')}
|
||||
active={view === 'docs'}
|
||||
>
|
||||
<DocsIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const WorkbenchHeader = ({ view, setView, saveAs = false, control = 4 }) => {
|
||||
const [dense, setDense] = useState(true)
|
||||
return (
|
||||
<MenuWrapper
|
||||
Icon={icons[view]}
|
||||
wrapperClass={`w-64 min-h-screen pt-4
|
||||
bg-neutral
|
||||
shrink-0 grow-0 self-stretch
|
||||
transition-all
|
||||
drop-shadow-xl
|
||||
${dense ? '-ml-52' : 'ml-0'}`}
|
||||
buttonClass={`order-last bottom-16`}
|
||||
keepOpenOnClick={false}
|
||||
order={0}
|
||||
type="nav"
|
||||
>
|
||||
<header
|
||||
className={`
|
||||
sticky top-4 lg:top-28
|
||||
group
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col
|
||||
items-center w-full `}
|
||||
>
|
||||
<NavIcons {...{ setView, setDense, dense, view, saveAs, control }} />
|
||||
</div>
|
||||
</header>
|
||||
</MenuWrapper>
|
||||
)
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { ClearIcon, EditIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
const EditCount = (props) => (
|
||||
<div className="form-control mb-2 w-full">
|
||||
<label className="label">
|
||||
<span className="label-text text-base-content">{props.min}</span>
|
||||
<span className="label-text font-bold text-base-content">{props.value}</span>
|
||||
<span className="label-text text-base-content">{props.max}</span>
|
||||
</label>
|
||||
<label className="input-group input-group-sm">
|
||||
<input
|
||||
type="number"
|
||||
className={`
|
||||
input input-sm input-bordered grow text-base-content
|
||||
`}
|
||||
value={props.value}
|
||||
onChange={props.handleChange}
|
||||
/>
|
||||
<span className="text-base-content font-bold">#</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const DesignOptionCount = (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const { count, max, min } = props.design.patternConfig.options[props.option]
|
||||
const val =
|
||||
typeof props.gist?.options?.[props.option] === 'undefined'
|
||||
? count
|
||||
: props.gist.options[props.option]
|
||||
|
||||
const [value, setValue] = useState(val)
|
||||
const [editCount, setEditCount] = useState(false)
|
||||
|
||||
const handleChange = (evt) => {
|
||||
const newVal = evt.target.value
|
||||
setValue(newVal)
|
||||
props.updateGist(['options', props.option], newVal)
|
||||
}
|
||||
const reset = () => {
|
||||
setValue(count)
|
||||
props.unsetGist(['options', props.option])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4 mx-6 border-l-2 pl-2">
|
||||
<div className="flex flex-row justify-between">
|
||||
{editCount ? (
|
||||
<EditCount
|
||||
value={value}
|
||||
handleChange={handleChange}
|
||||
min={min}
|
||||
max={max}
|
||||
setEditCount={setEditCount}
|
||||
t={t}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span className="opacity-50">{min}</span>
|
||||
<span className={`font-bold ${val === count ? 'text-secondary' : 'text-accent'}`}>
|
||||
{val}
|
||||
</span>
|
||||
<span className="opacity-50">{max}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
max={max}
|
||||
min={min}
|
||||
step={1}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className={`
|
||||
range range-sm mt-1
|
||||
${val === count ? 'range-secondary' : 'range-accent'}
|
||||
`}
|
||||
/>
|
||||
<div className="flex flex-row justify-between">
|
||||
<span></span>
|
||||
<div>
|
||||
<button
|
||||
title={t('reset')}
|
||||
className="btn btn-ghost btn-xs text-accent"
|
||||
disabled={val === count}
|
||||
onClick={reset}
|
||||
>
|
||||
<ClearIcon />
|
||||
</button>
|
||||
<button
|
||||
title={t('editThing', { thing: '#' })}
|
||||
className={`
|
||||
btn btn-ghost btn-xs hover:text-secondary-focus
|
||||
${editCount ? 'text-accent' : 'text-secondary'}
|
||||
`}
|
||||
onClick={() => setEditCount(!editCount)}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { ClearIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const DesignOptionList = (props) => {
|
||||
const { t } = useTranslation([`o_${props.design.designConfig.data.name}`])
|
||||
const { dflt, list, doNotTranslate = false } = props.design.patternConfig.options[props.option]
|
||||
const val =
|
||||
typeof props.gist?.options?.[props.option] === 'undefined'
|
||||
? dflt
|
||||
: props.gist.options[props.option]
|
||||
|
||||
const [value, setValue] = useState(val)
|
||||
|
||||
const handleChange = (newVal) => {
|
||||
if (newVal === dflt) reset()
|
||||
else {
|
||||
setValue(newVal)
|
||||
props.updateGist(['options', props.option], newVal)
|
||||
}
|
||||
}
|
||||
const reset = () => {
|
||||
setValue(dflt)
|
||||
props.unsetGist(['options', props.option])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4 mx-6 border-l-2 pl-2">
|
||||
<div className="flex flex-row">
|
||||
<div className="grow">
|
||||
{list.map((choice) => (
|
||||
<button
|
||||
key={choice}
|
||||
onClick={() => handleChange(choice)}
|
||||
className={`
|
||||
mr-1 mb-1 text-left text-lg w-full
|
||||
${
|
||||
choice === value
|
||||
? choice === dflt
|
||||
? 'text-secondary'
|
||||
: 'text-accent'
|
||||
: 'text-base-content'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
text-3xl mr-2 inline-block p-0 leading-3
|
||||
translate-y-3
|
||||
`}
|
||||
>
|
||||
<>°</>
|
||||
</span>
|
||||
{doNotTranslate
|
||||
? choice
|
||||
: props.ot(`o_${props.design.designConfig.data.name}:${props.option}.o.${choice}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button title={t('reset')} className="" disabled={val === dflt} onClick={reset}>
|
||||
<span className={val === dflt ? 'text-base' : 'text-accent'}>
|
||||
<ClearIcon />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DesignOptionList
|
|
@ -1,136 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { ClearIcon, EditIcon } from 'shared/components/icons.mjs'
|
||||
import { formatMm, round } from 'shared/utils.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
const EditOption = (props) => (
|
||||
<div className="form-control mb-2 w-full">
|
||||
<label className="label">
|
||||
<span className="label-text text-base-content">
|
||||
{props.min}
|
||||
{props.suffix}
|
||||
</span>
|
||||
<span className="label-text font-bold text-base-content">
|
||||
{props.value}
|
||||
{props.suffix}
|
||||
</span>
|
||||
<span className="label-text text-base-content">
|
||||
{props.max}
|
||||
{props.suffix}
|
||||
</span>
|
||||
</label>
|
||||
<label className="input-group input-group-sm">
|
||||
<input
|
||||
type="number"
|
||||
className={`
|
||||
input input-sm input-bordered grow text-base-content
|
||||
`}
|
||||
value={props.value}
|
||||
onChange={props.handleChange}
|
||||
/>
|
||||
<span className="text-base-content font-bold">{props.suffix}</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const DesignOptionPctDeg = (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const suffix = props.type === 'deg' ? '°' : '%'
|
||||
const factor = props.type === 'deg' ? 1 : 100
|
||||
const { max, min } = props.design.patternConfig.options[props.option]
|
||||
const dflt = props.design.patternConfig.options[props.option][props.type || 'pct']
|
||||
const val =
|
||||
typeof props.gist?.options?.[props.option] === 'undefined'
|
||||
? dflt
|
||||
: props.gist.options[props.option] * factor
|
||||
|
||||
const [value, setValue] = useState(val)
|
||||
const [editOption, setEditOption] = useState(false)
|
||||
|
||||
const handleChange = (evt) => {
|
||||
const newVal = evt.target.value
|
||||
setValue(newVal)
|
||||
props.updateGist(['options', props.option], newVal / factor)
|
||||
}
|
||||
const reset = () => {
|
||||
setValue(dflt)
|
||||
props.unsetGist(['options', props.option])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4 mx-6 border-l-2 pl-2">
|
||||
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
|
||||
{props.ot(`${props.option}.d`)}
|
||||
</p>
|
||||
<div className="flex flex-row justify-between">
|
||||
{editOption ? (
|
||||
<EditOption
|
||||
value={value}
|
||||
handleChange={handleChange}
|
||||
min={min}
|
||||
max={max}
|
||||
setEditOption={setEditOption}
|
||||
t={t}
|
||||
suffix={suffix}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span className="opacity-50">
|
||||
{round(min)}
|
||||
{suffix}
|
||||
</span>
|
||||
<span className={`font-bold ${val === dflt ? 'text-secondary' : 'text-accent'}`}>
|
||||
{round(val)}
|
||||
{suffix}
|
||||
</span>
|
||||
<span className="opacity-50">
|
||||
{round(max)}
|
||||
{suffix}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
max={max}
|
||||
min={min}
|
||||
step={0.1}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className={`
|
||||
range range-sm mt-1
|
||||
${val === dflt ? 'range-secondary' : 'range-accent'}
|
||||
`}
|
||||
/>
|
||||
<div className="flex flex-row justify-between">
|
||||
<span className={val === dflt ? 'text-secondary' : 'text-accent'}>
|
||||
{props.design.patternConfig.options[props.option]?.toAbs && props.gist.measurements
|
||||
? formatMm(
|
||||
props.design.patternConfig.options[props.option].toAbs(value / 100, props.gist)
|
||||
)
|
||||
: ' '}
|
||||
</span>
|
||||
<div>
|
||||
<button
|
||||
title={t('reset')}
|
||||
className="btn btn-ghost btn-xs text-accent"
|
||||
disabled={val === dflt}
|
||||
onClick={reset}
|
||||
>
|
||||
<ClearIcon />
|
||||
</button>
|
||||
<button
|
||||
title={t('editThing', { thing: suffix })}
|
||||
className={`
|
||||
btn btn-ghost btn-xs hover:text-secondary-focus
|
||||
${editOption ? 'text-accent' : 'text-secondary'}
|
||||
`}
|
||||
onClick={() => setEditOption(!editOption)}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { isDegreeMeasurement } from '../../../config/measurements'
|
||||
import { measurementAsMm } from 'shared/utils.mjs'
|
||||
|
||||
/*
|
||||
* This is a single input for a measurements
|
||||
* Note that it keeps local state with whatever the user types
|
||||
* but will only trigger a gist update if the input is valid.
|
||||
*
|
||||
* m holds the measurement name. It's just so long to type
|
||||
* measurement and I always have some typo in it because dyslexia.
|
||||
*/
|
||||
export const MeasurementInput = ({ m, gist, app, updateMeasurements, focus }) => {
|
||||
const { t } = useTranslation(['app', 'measurements'])
|
||||
const prefix = app.site === 'org' ? '' : 'https://freesewing.org'
|
||||
const title = t(`measurements:${m}`)
|
||||
|
||||
const isDegree = isDegreeMeasurement(m)
|
||||
const factor = useMemo(() => (isDegree ? 1 : gist.units == 'imperial' ? 25.4 : 10), [gist.units])
|
||||
|
||||
const isValValid = (val) =>
|
||||
typeof val === 'undefined' || val === '' ? null : val != false && !isNaN(val)
|
||||
const isValid = (newVal) => (typeof newVal === 'undefined' ? isValValid(val) : isValValid(newVal))
|
||||
|
||||
const [val, setVal] = useState(gist.measurements?.[m] / factor || '')
|
||||
|
||||
// keep a single reference to a debounce timer
|
||||
const debounceTimeout = useRef(null)
|
||||
const input = useRef(null)
|
||||
|
||||
// onChange
|
||||
const update = useCallback(
|
||||
(evt) => {
|
||||
evt.stopPropagation()
|
||||
let evtVal = evt.target.value
|
||||
// set Val immediately so that the input reflects it
|
||||
setVal(evtVal)
|
||||
|
||||
let useVal = isDegree ? evtVal : measurementAsMm(evtVal, gist.units)
|
||||
const ok = isValid(useVal)
|
||||
// only set to the gist if it's valid
|
||||
if (ok) {
|
||||
// debounce in case it's still changing
|
||||
if (debounceTimeout.current !== null) {
|
||||
clearTimeout(debounceTimeout.current)
|
||||
}
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
// clear the timeout reference
|
||||
debounceTimeout.current = null
|
||||
updateMeasurements(useVal, m)
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
[gist.units]
|
||||
)
|
||||
|
||||
// use this for better update efficiency
|
||||
const memoVal = useMemo(() => gist.measurements?.[m], [gist])
|
||||
// track validity against the value and the units
|
||||
const valid = useMemo(
|
||||
() => isValid(isDegree ? val : measurementAsMm(val, gist.units)),
|
||||
[val, gist.units]
|
||||
)
|
||||
|
||||
// hook to update the value or format when the gist changes
|
||||
useEffect(() => {
|
||||
// set the value to the proper value and format
|
||||
if (memoVal) {
|
||||
let gistVal = +(memoVal / factor).toFixed(2)
|
||||
setVal(gistVal)
|
||||
}
|
||||
}, [memoVal, factor])
|
||||
|
||||
// focus when prompted by parent
|
||||
useEffect(() => {
|
||||
if (focus) {
|
||||
input.current.focus()
|
||||
}
|
||||
}, [focus])
|
||||
|
||||
// cleanup
|
||||
useEffect(() => {
|
||||
clearTimeout(debounceTimeout.current)
|
||||
}, [])
|
||||
|
||||
if (!m) return null
|
||||
|
||||
return (
|
||||
<div className="form-control mb-2" key={`wrap-${m}`}>
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-xl">{title}</span>
|
||||
<a
|
||||
href={`${prefix}/docs/measurements/${m.toLowerCase()}`}
|
||||
className="label-text-alt text-secondary hover:text-secondary-focus hover:underline"
|
||||
title={`${t('docs')}: ${t(m)}`}
|
||||
tabIndex="-1"
|
||||
>
|
||||
{t('docs')}
|
||||
</a>
|
||||
</label>
|
||||
<label className="input-group input-group-lg">
|
||||
<input
|
||||
key={`input-${m}`}
|
||||
ref={input}
|
||||
type="text"
|
||||
placeholder={title}
|
||||
className={`
|
||||
input input-lg input-bordered grow text-base-content border-r-0
|
||||
${valid === false && 'input-error'}
|
||||
${valid === true && 'input-success'}
|
||||
`}
|
||||
value={val}
|
||||
onChange={update}
|
||||
/>
|
||||
<span
|
||||
role="img"
|
||||
className={`bg-transparent border-y
|
||||
${valid === false && 'border-error text-neutral-content'}
|
||||
${valid === true && 'border-success text-neutral-content'}
|
||||
${valid === null && 'border-base-200 text-base-content'}
|
||||
`}
|
||||
>
|
||||
{valid === true && '👍'}
|
||||
{valid === false && '🤔'}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
${valid === false && 'bg-error text-neutral-content'}
|
||||
${valid === true && 'bg-success text-neutral-content'}
|
||||
${valid === null && 'bg-base-200 text-base-content'}
|
||||
`}
|
||||
>
|
||||
{isDegree ? '° ' : gist.units == 'metric' ? 'cm' : 'in'}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import { CutLayoutSettings } from './settings.mjs'
|
||||
import { Draft } from '../draft/index.mjs'
|
||||
import { fabricPlugin } from '../plugin-layout-part.mjs'
|
||||
import { cutLayoutPlugin } from './plugin-cut-layout.mjs'
|
||||
import { pluginAnnotations } from '@freesewing/plugin-annotations'
|
||||
import { measurementAsMm } from 'shared/utils.mjs'
|
||||
import { useEffect } from 'react'
|
||||
import get from 'lodash.get'
|
||||
|
||||
export const fabricSettingsOrDefault = (gist, fabric) => {
|
||||
const isImperial = gist.units === 'imperial'
|
||||
const sheetHeight = measurementAsMm(isImperial ? 36 : 100, gist.units)
|
||||
const gistSettings = get(gist, ['_state', 'layout', 'forCutting', 'fabric', fabric])
|
||||
const sheetWidth = gistSettings?.sheetWidth || measurementAsMm(isImperial ? 54 : 120, gist.units)
|
||||
const grainDirection =
|
||||
gistSettings?.grainDirection === undefined ? 90 : gistSettings.grainDirection
|
||||
|
||||
return { activeFabric: fabric, sheetWidth, grainDirection, sheetHeight }
|
||||
}
|
||||
|
||||
const activeFabricPath = ['_state', 'layout', 'forCutting', 'activeFabric']
|
||||
const useFabricSettings = (gist) => {
|
||||
const activeFabric = get(gist, activeFabricPath) || 'fabric'
|
||||
return fabricSettingsOrDefault(gist, activeFabric)
|
||||
}
|
||||
|
||||
const useFabricDraft = (gist, design, fabricSettings) => {
|
||||
// get the appropriate layout for the view
|
||||
const layout =
|
||||
get(gist, ['layouts', gist._state.view, fabricSettings.activeFabric]) || gist.layout || true
|
||||
// hand it separately to the design
|
||||
const draft = new design({ ...gist, layout })
|
||||
|
||||
const layoutSettings = {
|
||||
sheetWidth: fabricSettings.sheetWidth,
|
||||
sheetHeight: fabricSettings.sheetHeight,
|
||||
}
|
||||
|
||||
let patternProps
|
||||
try {
|
||||
// add the fabric plugin to the draft
|
||||
draft.use(fabricPlugin(layoutSettings))
|
||||
// add the cutLayout plugin
|
||||
draft.use(cutLayoutPlugin(fabricSettings.activeFabric, fabricSettings.grainDirection))
|
||||
// also, pluginAnnotations and pluginFlip are needed
|
||||
draft.use(pluginAnnotations)
|
||||
|
||||
// draft the pattern
|
||||
draft.draft()
|
||||
patternProps = draft.getRenderProps()
|
||||
} catch (err) {
|
||||
console.log(err, gist)
|
||||
}
|
||||
|
||||
return { draft, patternProps }
|
||||
}
|
||||
|
||||
const useFabricList = (draft) => {
|
||||
return draft.setStores[0].cutlist.getCutFabrics(draft.settings[0])
|
||||
}
|
||||
|
||||
const bgProps = { fill: 'none' }
|
||||
export const CutLayout = (props) => {
|
||||
const { t } = useTranslation(['workbench', 'plugin'])
|
||||
const { gist, design, updateGist } = props
|
||||
|
||||
// disable xray
|
||||
useEffect(() => {
|
||||
if (gist?._state?.xray?.enabled) updateGist(['_state', 'xray', 'enabled'], false)
|
||||
})
|
||||
|
||||
const fabricSettings = useFabricSettings(gist)
|
||||
const { draft, patternProps } = useFabricDraft(gist, design, fabricSettings)
|
||||
const fabricList = useFabricList(draft)
|
||||
|
||||
const setCutFabric = (newFabric) => {
|
||||
updateGist(activeFabricPath, newFabric)
|
||||
}
|
||||
|
||||
let name = design.designConfig.data.name
|
||||
name = name.replace('@freesewing/', '')
|
||||
|
||||
const settingsProps = {
|
||||
gist,
|
||||
updateGist,
|
||||
patternProps,
|
||||
unsetGist: props.unsetGist,
|
||||
...fabricSettings,
|
||||
}
|
||||
|
||||
return patternProps ? (
|
||||
<div>
|
||||
<h2 className="capitalize">{t('layoutThing', { thing: name }) + ': ' + t('forCutting')}</h2>
|
||||
<CutLayoutSettings {...settingsProps} />
|
||||
<div className="my-4">
|
||||
{fabricList.length > 1 ? (
|
||||
<div className="tabs">
|
||||
{fabricList.map((title) => (
|
||||
<button
|
||||
key={title}
|
||||
className={`text-xl font-bold capitalize tab tab-bordered grow ${
|
||||
fabricSettings.activeFabric === title ? 'tab-active' : ''
|
||||
}`}
|
||||
onClick={() => setCutFabric(title)}
|
||||
>
|
||||
{t('plugin:' + title)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<Draft
|
||||
draft={draft}
|
||||
gist={gist}
|
||||
updateGist={updateGist}
|
||||
patternProps={patternProps}
|
||||
bgProps={bgProps}
|
||||
gistReady={props.gistReady}
|
||||
layoutPart="fabric"
|
||||
layoutType={['cuttingLayout', fabricSettings.activeFabric]}
|
||||
layoutSetType="forCutting"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
import { addToOnly } from '../plugin-layout-part.mjs'
|
||||
import { pluginMirror } from '@freesewing/plugin-mirror'
|
||||
const prefix = 'mirroredOnFold'
|
||||
|
||||
// types of path operations
|
||||
const opTypes = ['to', 'from', 'cp1', 'cp2']
|
||||
const avoidRegx = new RegExp(`^(cutonfold|grainline|__scalebox|__miniscale|${prefix})`)
|
||||
|
||||
/**
|
||||
* The plugin to handle all business related to mirroring, rotating, and duplicating parts for the cutting layout
|
||||
* @param {string} material the material to generate a cutting layout for
|
||||
* @param {number} grainAngle the angle of the material's grain
|
||||
* @return {Object} the plugin
|
||||
*/
|
||||
export const cutLayoutPlugin = function (material, grainAngle) {
|
||||
return {
|
||||
hooks: {
|
||||
// after each part
|
||||
postPartDraft: (pattern) => {
|
||||
// if it's a duplicated cut part, the fabric part, or it's not wanted by the pattern
|
||||
if (
|
||||
pattern.activePart.startsWith('cut.') ||
|
||||
pattern.activePart === 'fabric' ||
|
||||
!pattern.__wants(pattern.activePart)
|
||||
)
|
||||
return
|
||||
|
||||
// get the part that's just been drafted
|
||||
const part = pattern.parts[pattern.activeSet][pattern.activePart]
|
||||
// get this part's cutlist configuration
|
||||
let partCutlist = pattern.setStores[pattern.activeSet].get(['cutlist', pattern.activePart])
|
||||
// if there isn't one, we're done here
|
||||
if (!partCutlist) return
|
||||
|
||||
// if the cutlist has materials but this isn't one of them
|
||||
// or it has no materials but this isn't the main fabric
|
||||
if (partCutlist.materials ? !partCutlist.materials[material] : material !== 'fabric') {
|
||||
// hide the part because it shouldn't be shown on this fabric
|
||||
part.hide()
|
||||
return
|
||||
}
|
||||
|
||||
// get the cutlist configuration for this material, or default to one
|
||||
const matCutConfig =
|
||||
partCutlist.materials?.[material] || (material === 'fabric' ? [{ cut: 1 }] : [])
|
||||
|
||||
// get the config of the active part to be inherited by all duplicates
|
||||
const activePartConfig = pattern.config.parts[pattern.activePart]
|
||||
|
||||
// hide the active part so that all others can inherit from it and be manipulated separately
|
||||
part.hide()
|
||||
|
||||
// for each set of cutting instructions for this material
|
||||
matCutConfig.forEach((instruction, i) => {
|
||||
// for each piece that should be cut
|
||||
for (let c = 0; c < instruction.cut; c++) {
|
||||
const dupPartName = `cut.${pattern.activePart}.${material}_${c + i + 1}`
|
||||
|
||||
// make a new part that will follow these cutting instructions
|
||||
pattern.addPart({
|
||||
name: dupPartName,
|
||||
from: activePartConfig,
|
||||
draft: ({ part, macro, utils }) => {
|
||||
part.attributes.remove('transform')
|
||||
|
||||
// if they shouldn't be identical, flip every other piece
|
||||
if (!instruction.identical && c % 2 === 1) {
|
||||
part.attributes.add(
|
||||
'transform',
|
||||
grainAngle === 90 ? 'scale(-1, 1)' : 'scale(1, -1)'
|
||||
)
|
||||
}
|
||||
|
||||
macro('handleFoldAndGrain', {
|
||||
partCutlist,
|
||||
instruction,
|
||||
})
|
||||
|
||||
// combine the transforms
|
||||
const combinedTransform = utils.combineTransforms(
|
||||
part.attributes.getAsArray('transform')
|
||||
)
|
||||
part.attributes.set('transform', combinedTransform)
|
||||
|
||||
return part
|
||||
},
|
||||
})
|
||||
|
||||
// add it to the only list if there is one
|
||||
addToOnly(pattern, dupPartName)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
macros: {
|
||||
...pluginMirror.macros,
|
||||
// handle mirroring on the fold and rotating to sit along the grain or bias
|
||||
handleFoldAndGrain: ({ partCutlist, instruction }, { points, macro }) => {
|
||||
// get the grain angle for the part for this set of instructions
|
||||
const grainSpec = partCutlist.grain
|
||||
? partCutlist.grain + (instruction.bias ? 45 : 0)
|
||||
: undefined
|
||||
// if the part has cutonfold instructions
|
||||
if (partCutlist.cutOnFold) {
|
||||
// if we're not meant to igore those instructions, mirror on the fold
|
||||
if (!instruction.ignoreOnFold) macro('mirrorOnFold', { fold: partCutlist.cutOnFold })
|
||||
// if we are meant to ignore those instructions, but there's a grainline
|
||||
else if (grainSpec !== undefined) {
|
||||
// replace the cutonfold with a grainline
|
||||
macro('grainline', { from: points.cutonfoldVia1, to: points.cutonfoldVia2 })
|
||||
macro('cutonfold', false)
|
||||
}
|
||||
}
|
||||
|
||||
// if there's a grain angle, rotate the part to be along it
|
||||
macro('rotateToGrain', { bias: instruction.bias, grainSpec })
|
||||
},
|
||||
// mirror the part across the line indicated by cutonfold
|
||||
mirrorOnFold: ({ fold }, { paths, snippets, macro, points, utils, Point }) => {
|
||||
// get all the paths to mirror
|
||||
const mirrorPaths = []
|
||||
for (const p in paths) {
|
||||
// skip ones that are hidden
|
||||
if (!paths[p].hidden && !p.match(avoidRegx)) mirrorPaths.push(p)
|
||||
}
|
||||
|
||||
// store all the points to mirror
|
||||
const mirrorPoints = []
|
||||
// store snippets by type so we can re-sprinkle later
|
||||
const snippetsByType = {}
|
||||
// for each snippet
|
||||
let anchorNames = 0
|
||||
for (var s in snippets) {
|
||||
const snip = snippets[s]
|
||||
// don't mirror these ones
|
||||
if (['logo'].indexOf(snip.def) > -1) continue
|
||||
|
||||
// get or make an array for this type of snippet
|
||||
snippetsByType[snip.def] = snippetsByType[snip.def] || []
|
||||
|
||||
// put the anchor on the list to mirror
|
||||
const anchorName = `snippetAnchors_${anchorNames++}`
|
||||
points[anchorName] = new Point(snip.anchor.x, snip.anchor.y)
|
||||
mirrorPoints.push(anchorName)
|
||||
snippetsByType[snip.def].push(`${prefix}${utils.capitalize(anchorName)}`)
|
||||
}
|
||||
|
||||
// mirror
|
||||
macro('mirror', {
|
||||
paths: mirrorPaths,
|
||||
points: mirrorPoints,
|
||||
mirror: fold,
|
||||
prefix,
|
||||
})
|
||||
|
||||
// sprinkle the snippets
|
||||
for (var def in snippetsByType) {
|
||||
macro('sprinkle', {
|
||||
snippet: def,
|
||||
on: snippetsByType[def],
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* rotate the part so that it is oriented properly with regard to the fabric grain
|
||||
* if the part should be on the bias, this rotates the part to lie on the bias
|
||||
* while keeping the grainline annotation along the grain
|
||||
*/
|
||||
rotateToGrain: ({ bias, grainSpec }, { part, paths, points, Point }) => {
|
||||
// the amount to rotate is the difference between this part's grain angle (as drafted) and the fabric's grain angle
|
||||
let toRotate = grainSpec === undefined ? 0 : grainAngle + grainSpec
|
||||
// don't over rotate
|
||||
toRotate = toRotate % 180
|
||||
if (toRotate < 0) toRotate += 180
|
||||
// if there's no difference, don't rotate
|
||||
if (toRotate === 0) return
|
||||
|
||||
if (paths.grainline && bias) {
|
||||
const pivot = points.grainlineFrom || new Point(0, 0)
|
||||
paths.grainline.ops.forEach((op) => {
|
||||
opTypes.forEach((t) => {
|
||||
if (op[t]) op[t] = op[t].rotate(45, pivot)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
part.attributes.add('transform', `rotate(${toRotate})`)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
import { ClearIcon, IconWrapper } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { formatFraction128, measurementAsMm, round, formatMm } from 'shared/utils.mjs'
|
||||
import { ShowButtonsToggle } from '../draft/buttons.mjs'
|
||||
|
||||
const SheetIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z"
|
||||
/>
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
const GrainIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<g>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 17l-5-5 5-5" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12l8 0" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M18 7l5 5-5 5" />
|
||||
</g>
|
||||
</IconWrapper>
|
||||
)
|
||||
const FabricSizer = ({ gist, updateGist, activeFabric, sheetWidth }) => {
|
||||
const { t } = useTranslation(['workbench'])
|
||||
|
||||
let val = formatMm(sheetWidth, gist.units, 'none')
|
||||
// onChange
|
||||
const update = (evt) => {
|
||||
evt.stopPropagation()
|
||||
let evtVal = evt.target.value
|
||||
// set Val immediately so that the input reflects it
|
||||
val = evtVal
|
||||
|
||||
let useVal = measurementAsMm(evtVal, gist.units)
|
||||
// only set to the gist if it's valid
|
||||
if (!isNaN(useVal)) {
|
||||
updateGist(['_state', 'layout', 'forCutting', 'fabric', activeFabric, 'sheetWidth'], useVal)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<label className="input-group">
|
||||
<span className="label-text font-bold">{`${t(activeFabric)} ${t('width')}`}</span>
|
||||
<input
|
||||
key="input-fabricWidth"
|
||||
type="text"
|
||||
className="input input-bordered grow text-base-content border-r-0 w-20"
|
||||
value={val}
|
||||
onChange={update}
|
||||
/>
|
||||
<span className="bg-transparent border input-bordered">
|
||||
{gist.units == 'metric' ? 'cm' : 'in'}
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
export const GrainDirectionPicker = ({ grainDirection, activeFabric, updateGist }) => {
|
||||
const { t } = useTranslation(['workbench'])
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
btn btn-primary flex flex-row gap-2 items-center
|
||||
hover:text-primary-content ml-4
|
||||
`}
|
||||
onClick={() =>
|
||||
updateGist(
|
||||
['_state', 'layout', 'forCutting', 'fabric', activeFabric, 'grainDirection'],
|
||||
grainDirection === 0 ? 90 : 0
|
||||
)
|
||||
}
|
||||
>
|
||||
<GrainIcon className={`h-6 w-6 mr-2 ${grainDirection === 0 ? '' : 'rotate-90'}`} />
|
||||
<span>{t(`grainDirection`)}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const useFabricLength = (isImperial, height, format = 'none') => {
|
||||
// regular conversion from mm to inches or cm
|
||||
const unit = isImperial ? 25.4 : 10
|
||||
// conversion from inches or cm to yards or meters
|
||||
const fabricUnit = isImperial ? 36 : 100
|
||||
// for fabric, these divisions are granular enough
|
||||
const rounder = isImperial ? 16 : 10
|
||||
|
||||
// we convert the used fabric height to the right units so we can round it
|
||||
const inFabricUnits = height / (fabricUnit * unit)
|
||||
// we multiply it by the rounder, round it up, then divide by the rounder again to get the rounded amount
|
||||
const roundCount = Math.ceil(rounder * inFabricUnits) / rounder
|
||||
// format as a fraction for imperial, a decimal for metric
|
||||
const count = isImperial ? formatFraction128(roundCount, format) : round(roundCount, 1)
|
||||
|
||||
return `${count}${isImperial ? 'yds' : 'm'}`
|
||||
}
|
||||
|
||||
export const CutLayoutSettings = ({
|
||||
gist,
|
||||
patternProps,
|
||||
unsetGist,
|
||||
updateGist,
|
||||
activeFabric,
|
||||
sheetWidth,
|
||||
grainDirection,
|
||||
}) => {
|
||||
const { t } = useTranslation(['workbench'])
|
||||
|
||||
const fabricLength = useFabricLength(gist.units === 'imperial', patternProps.height)
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between mb-2 items-center">
|
||||
<div className="flex">
|
||||
<FabricSizer {...{ gist, updateGist, activeFabric, sheetWidth }} />
|
||||
<GrainDirectionPicker {...{ updateGist, activeFabric, grainDirection }} />
|
||||
</div>
|
||||
<div>
|
||||
<SheetIcon className="h-6 w-6 mr-2 inline align-middle" />
|
||||
<span className="text-xl font-bold align-middle">{fabricLength}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<ShowButtonsToggle
|
||||
gist={gist}
|
||||
updateGist={updateGist}
|
||||
layoutSetType="forCutting"
|
||||
></ShowButtonsToggle>
|
||||
<button
|
||||
key="reset"
|
||||
onClick={() => unsetGist(['layouts', 'cuttingLayout', activeFabric])}
|
||||
className="btn btn-primary btn-outline ml-4"
|
||||
>
|
||||
<ClearIcon className="h-6 w-6 mr-2" />
|
||||
{t('reset')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -7,8 +7,8 @@ import get from 'lodash.get'
|
|||
import Link from 'next/link'
|
||||
import { PrimaryNavigation } from 'shared/components/navigation/primary.mjs'
|
||||
import { RightIcon, LeftIcon, FreeSewingIcon } from 'shared/components/icons.mjs'
|
||||
import { Header } from 'site/components/header.mjs'
|
||||
import { Footer } from 'site/components/footer.mjs'
|
||||
import { Header } from 'site/components/header/index.mjs'
|
||||
import { Footer } from 'shared/components/footer/index.mjs'
|
||||
import { Search } from 'site/components/search.mjs'
|
||||
|
||||
export const PageTitle = ({ app, slug, title }) => {
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
import { useRef } from 'react'
|
||||
import { Stack } from './stack.mjs'
|
||||
import { SvgWrapper } from '../../draft/svg.mjs'
|
||||
import { PartInner } from '../../draft/part.mjs'
|
||||
import get from 'lodash.get'
|
||||
|
||||
export const Draft = (props) => {
|
||||
const {
|
||||
patternProps,
|
||||
gist,
|
||||
updateGist,
|
||||
app,
|
||||
bgProps = {},
|
||||
fitLayoutPart = false,
|
||||
layoutType = 'printingLayout',
|
||||
layoutSetType = 'forPrinting',
|
||||
} = props
|
||||
|
||||
const svgRef = useRef(null)
|
||||
if (!patternProps) return null
|
||||
// keep a fresh copy of the layout because we might manipulate it without saving to the gist
|
||||
const layoutPath = ['layouts'].concat(layoutType)
|
||||
let layout = get(patternProps.settings[0], layoutPath) || {
|
||||
...patternProps.autoLayout,
|
||||
width: patternProps.width,
|
||||
height: patternProps.height,
|
||||
}
|
||||
|
||||
// Helper method to update part layout and re-calculate width * height
|
||||
const updateLayout = (name, config, history = true) => {
|
||||
// Start creating new layout
|
||||
const newLayout = { ...layout }
|
||||
newLayout.stacks[name] = config
|
||||
|
||||
// Pattern topLeft and bottomRight
|
||||
let topLeft = { x: 0, y: 0 }
|
||||
let bottomRight = { x: 0, y: 0 }
|
||||
for (const pname in patternProps.stacks) {
|
||||
if (pname == props.layoutPart && !fitLayoutPart) continue
|
||||
let partLayout = newLayout.stacks[pname]
|
||||
|
||||
// Pages part does not have its topLeft and bottomRight set by core since it's added post-draft
|
||||
if (partLayout?.tl) {
|
||||
// set the pattern extremes
|
||||
topLeft.x = Math.min(topLeft.x, partLayout.tl.x)
|
||||
topLeft.y = Math.min(topLeft.y, partLayout.tl.y)
|
||||
bottomRight.x = Math.max(bottomRight.x, partLayout.br.x)
|
||||
bottomRight.y = Math.max(bottomRight.y, partLayout.br.y)
|
||||
}
|
||||
}
|
||||
|
||||
newLayout.width = bottomRight.x - topLeft.x
|
||||
newLayout.height = bottomRight.y - topLeft.y
|
||||
newLayout.bottomRight = bottomRight
|
||||
newLayout.topLeft = topLeft
|
||||
|
||||
if (history) {
|
||||
updateGist(layoutPath, newLayout, history)
|
||||
} else {
|
||||
// we don't put it in the gist if it shouldn't contribute to history because we need some of the data calculated here for rendering purposes on the initial layout, but we don't want to actually save a layout until the user manipulates it. This is what allows the layout to respond appropriately to settings changes. Once the user has starting playing with the layout, all bets are off
|
||||
layout = newLayout
|
||||
}
|
||||
}
|
||||
|
||||
const viewBox = layout.topLeft
|
||||
? `${layout.topLeft.x} ${layout.topLeft.y} ${layout.width} ${layout.height}`
|
||||
: false
|
||||
|
||||
// We need to make sure the `pages` part is at the bottom of the pile
|
||||
// so we can drag-drop all parts on top of it.
|
||||
// Bottom in SVG means we need to draw it first
|
||||
const stacks = [
|
||||
<PartInner
|
||||
{...{
|
||||
part: patternProps.parts[0][props.layoutPart],
|
||||
partName: props.layoutPart,
|
||||
gist,
|
||||
skipGrid: true,
|
||||
}}
|
||||
key={props.layoutPart}
|
||||
/>,
|
||||
]
|
||||
|
||||
// then make a stack component for each remaining stack
|
||||
for (var stackName in patternProps.stacks) {
|
||||
if (stackName === props.layoutPart) {
|
||||
continue
|
||||
}
|
||||
let stack = patternProps.stacks[stackName]
|
||||
|
||||
const stackPart = (
|
||||
<Stack
|
||||
{...{
|
||||
key: stackName,
|
||||
stackName,
|
||||
stack,
|
||||
layout,
|
||||
app,
|
||||
gist,
|
||||
updateLayout,
|
||||
isLayoutPart: stackName === props.layoutPart,
|
||||
layoutSetType: layoutSetType,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
stacks.push(stackPart)
|
||||
}
|
||||
|
||||
return (
|
||||
<SvgWrapper {...{ patternProps, gist, viewBox }} ref={svgRef}>
|
||||
<rect x="0" y="0" width={layout.width} height={layout.height} {...bgProps} />
|
||||
{stacks}
|
||||
</SvgWrapper>
|
||||
)
|
||||
}
|
|
@ -1,260 +0,0 @@
|
|||
/*
|
||||
* This React component is a long way from perfect, but it's a start for
|
||||
* handling custom layouts.
|
||||
*
|
||||
* There are a few reasons that (at least in my opinion) implementing this is non-trivial:
|
||||
*
|
||||
* 1) React re-render vs DOM updates
|
||||
*
|
||||
* For performance reasons, we can't re-render with React when the user drags a
|
||||
* pattern part (or rotates it). It would kill performance.
|
||||
* So, we don't re-render with React upon dragging/rotating, but instead manipulate
|
||||
* the DOM directly.
|
||||
*
|
||||
* So far so good, but of course we don't want a pattern that's only correctly laid
|
||||
* out in the DOM. We want to update the pattern gist so that the new layout is stored.
|
||||
* For this, we re-render with React on the end of the drag (or rotate).
|
||||
*
|
||||
* Handling this balance between DOM updates and React re-renders is a first contributing
|
||||
* factor to why this component is non-trivial
|
||||
*
|
||||
* 2) SVG vs DOM coordinates
|
||||
*
|
||||
* When we drag or rotate with the mouse, all the events are giving us coordinates of
|
||||
* where the mouse is in the DOM.
|
||||
*
|
||||
* The layout uses coordinates from the embedded SVG which are completely different.
|
||||
*
|
||||
* We run `getScreenCTM().inverse()` on the svg element to pass to `matrixTransform` on a `DOMPointReadOnly` for dom to svg space conversions.
|
||||
*
|
||||
* 3) Part-level transforms
|
||||
*
|
||||
* All parts use their center as the transform-origin to simplify transforms, especially flipping and rotating.
|
||||
*
|
||||
* 4) Bounding box
|
||||
*
|
||||
* We use `getBoundingClientRect` rather than `getBBox` because it provides more data and factors in the transforms.
|
||||
* We then use our `domToSvg` function to move the points back into the SVG space.
|
||||
*
|
||||
*
|
||||
* Known issues
|
||||
* - currently none
|
||||
*
|
||||
* I've sort of left it at this because I'm starting to wonder if we should perhaps re-think
|
||||
* how custom layouts are supported in the core. And I would like to discuss this with the core team.
|
||||
*/
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
import { generateStackTransform, getTransformedBounds } from '@freesewing/core'
|
||||
import { Part } from '../../draft/part.mjs'
|
||||
import { getProps, angle } from '../../draft/utils.mjs'
|
||||
import { drag } from 'd3-drag'
|
||||
import { select } from 'd3-selection'
|
||||
import { Buttons } from './buttons.mjs'
|
||||
import get from 'lodash.get'
|
||||
|
||||
export const Stack = (props) => {
|
||||
const { layout, stack, stackName, gist } = props
|
||||
|
||||
const stackLayout = layout.stacks?.[stackName]
|
||||
const stackExists = typeof stackLayout?.move?.x !== 'undefined'
|
||||
|
||||
// Use a ref for direct DOM manipulation
|
||||
const stackRef = useRef(null)
|
||||
const centerRef = useRef(null)
|
||||
const innerRef = useRef(null)
|
||||
|
||||
// State variable to switch between moving or rotating the part
|
||||
const [rotate, setRotate] = useState(false)
|
||||
|
||||
// update the layout on mount
|
||||
useEffect(() => {
|
||||
// only update if there's a rendered part and it's not the pages or fabric part
|
||||
if (stackRef.current && !props.isLayoutPart) {
|
||||
updateLayout(false)
|
||||
}
|
||||
}, [stackRef, stackLayout])
|
||||
|
||||
// Initialize drag handler
|
||||
useEffect(() => {
|
||||
// don't drag the pages
|
||||
if (props.isLayoutPart || !stackExists) return
|
||||
handleDrag(select(stackRef.current))
|
||||
}, [rotate, stackRef, stackLayout])
|
||||
|
||||
// // Don't just assume this makes sense
|
||||
if (!stackExists) return null
|
||||
|
||||
// These are kept as vars because re-rendering on drag would kill performance
|
||||
// Managing the difference between re-render and direct DOM updates makes this
|
||||
// whole thing a bit tricky to wrap your head around
|
||||
let translateX = stackLayout.move.x
|
||||
let translateY = stackLayout.move.y
|
||||
let stackRotation = stackLayout.rotate || 0
|
||||
let rotation = stackRotation
|
||||
let flipX = !!stackLayout.flipX
|
||||
let flipY = !!stackLayout.flipY
|
||||
|
||||
const center = {
|
||||
x: stack.topLeft.x + (stack.bottomRight.x - stack.topLeft.x) / 2,
|
||||
y: stack.topLeft.y + (stack.bottomRight.y - stack.topLeft.y) / 2,
|
||||
}
|
||||
|
||||
/** get the delta rotation from the start of the drag event to now */
|
||||
const getRotation = (event) =>
|
||||
angle(center, event.subject) - angle(center, { x: event.x, y: event.y })
|
||||
|
||||
const setTransforms = () => {
|
||||
// get the transform attributes
|
||||
const transforms = generateStackTransform(translateX, translateY, rotation, flipX, flipY, stack)
|
||||
|
||||
const me = select(stackRef.current)
|
||||
me.attr('transform', transforms.join(' '))
|
||||
|
||||
return transforms
|
||||
}
|
||||
|
||||
let didDrag = false
|
||||
const handleDrag = drag()
|
||||
// subject allows us to save data from the start of the event to use throughout event handing
|
||||
.subject(function (event) {
|
||||
return rotate
|
||||
? // if we're rotating, the subject is the mouse position
|
||||
{ x: event.x, y: event.y }
|
||||
: // if we're moving, the subject is the part's x,y coordinates
|
||||
{ x: translateX, y: translateY }
|
||||
})
|
||||
.on('drag', function (event) {
|
||||
if (!event.dx && !event.dy) return
|
||||
|
||||
if (rotate) {
|
||||
let newRotation = getRotation(event)
|
||||
// shift key to snap the rotation
|
||||
if (event.sourceEvent.shiftKey) {
|
||||
newRotation = Math.ceil(newRotation / 15) * 15
|
||||
}
|
||||
// reverse the rotation direction one time per flip. if we're flipped both directions, rotation will be positive again
|
||||
if (flipX) newRotation *= -1
|
||||
if (flipY) newRotation *= -1
|
||||
|
||||
rotation = stackRotation + newRotation
|
||||
} else {
|
||||
translateX = event.x
|
||||
translateY = event.y
|
||||
}
|
||||
|
||||
// a drag happened, so we should update the layout when we're done
|
||||
didDrag = true
|
||||
setTransforms()
|
||||
})
|
||||
.on('end', function () {
|
||||
// save to gist if anything actually changed
|
||||
if (didDrag) updateLayout()
|
||||
|
||||
didDrag = false
|
||||
})
|
||||
|
||||
/** reset the part's transforms */
|
||||
const resetPart = () => {
|
||||
rotation = 0
|
||||
flipX = 0
|
||||
flipY = 0
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
/** toggle between dragging and rotating */
|
||||
const toggleDragRotate = () => {
|
||||
// only respond if the part should be able to drag/rotate
|
||||
if (!stackRef.current || props.isLayoutPart) {
|
||||
return
|
||||
}
|
||||
|
||||
setRotate(!rotate)
|
||||
}
|
||||
|
||||
/** update the layout either locally or in the gist */
|
||||
const updateLayout = (history = true) => {
|
||||
/** don't mess with what we don't lay out */
|
||||
if (!stackRef.current || props.isLayoutPart) return
|
||||
|
||||
// set the transforms on the stack in order to calculate from the latest position
|
||||
const transforms = setTransforms()
|
||||
|
||||
// apply the transforms to the bounding box to get the new extents of the stack
|
||||
const { tl, br } = getTransformedBounds(stack, transforms)
|
||||
|
||||
// update it on the draft component
|
||||
props.updateLayout(
|
||||
stackName,
|
||||
{
|
||||
move: {
|
||||
x: translateX,
|
||||
y: translateY,
|
||||
},
|
||||
rotate: rotation % 360,
|
||||
flipX,
|
||||
flipY,
|
||||
tl,
|
||||
br,
|
||||
},
|
||||
history
|
||||
)
|
||||
}
|
||||
|
||||
/** Method to flip (mirror) the part along the X or Y axis */
|
||||
const flip = (axis) => {
|
||||
if (axis === 'x') flipX = !flipX
|
||||
else flipY = !flipY
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
/** method to rotate 90 degrees */
|
||||
const rotate90 = (direction = 1) => {
|
||||
if (flipX) direction *= -1
|
||||
if (flipY) direction *= -1
|
||||
|
||||
rotation += 90 * direction
|
||||
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
// don't render if the part is empty
|
||||
// if (Object.keys(part.snippets).length === 0 && Object.keys(part.paths).length === 0) return null;
|
||||
|
||||
const showButtons = get(gist, ['_state', 'layout', props.layoutSetType, 'showButtons'], true)
|
||||
return (
|
||||
<g id={`stack-${stackName}`} {...getProps(stack)} ref={stackRef}>
|
||||
<g id={`stack-inner-${stackName}`} ref={innerRef}>
|
||||
{stack.parts.map((part) => (
|
||||
<Part {...{ part, partName: part.name, gist }} key={part.name}></Part>
|
||||
))}
|
||||
</g>
|
||||
{!props.isLayoutPart && (
|
||||
<>
|
||||
<text x={center.x} y={center.y} ref={centerRef} />
|
||||
<rect
|
||||
x={stack.topLeft.x}
|
||||
y={stack.topLeft.y}
|
||||
width={stack.width}
|
||||
height={stack.height}
|
||||
className={`layout-rect ${rotate ? 'rotate' : 'move'}`}
|
||||
id={`${stackName}-layout-rect`}
|
||||
onClick={toggleDragRotate}
|
||||
/>
|
||||
{showButtons ? (
|
||||
<Buttons
|
||||
transform={`translate(${center.x}, ${center.y}) rotate(${-rotation}) scale(${
|
||||
flipX ? -1 : 1
|
||||
},${flipY ? -1 : 1})`}
|
||||
flip={flip}
|
||||
rotate={rotate}
|
||||
setRotate={setRotate}
|
||||
resetPart={resetPart}
|
||||
rotate90={rotate90}
|
||||
partName={stackName}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
}
|
|
@ -1,432 +0,0 @@
|
|||
import { pluginAnnotations } from '@freesewing/plugin-annotations'
|
||||
|
||||
const name = 'Pages Plugin'
|
||||
const version = '1.0.0'
|
||||
export const sizes = {
|
||||
a4: [210, 297],
|
||||
a3: [297, 420],
|
||||
a2: [420, 594],
|
||||
a1: [594, 841],
|
||||
a0: [841, 1188],
|
||||
letter: [215.9, 279.4],
|
||||
tabloid: [279.4, 431.8],
|
||||
}
|
||||
|
||||
/** get a letter to represent an index less than 26*/
|
||||
const indexLetter = (i) => String.fromCharCode('A'.charCodeAt(0) + i - 1)
|
||||
|
||||
/** get a string of letters to represent an index */
|
||||
const indexStr = (i) => {
|
||||
let index = i % 26
|
||||
let quotient = i / 26
|
||||
let result
|
||||
|
||||
if (i <= 26) {
|
||||
return indexLetter(i)
|
||||
} //Number is within single digit bounds of our encoding letter alphabet
|
||||
|
||||
if (quotient >= 1) {
|
||||
//This number was bigger than the alphabet, recursively perform this function until we're done
|
||||
if (index === 0) {
|
||||
quotient--
|
||||
} //Accounts for the edge case of the last letter in the dictionary string
|
||||
result = indexStr(quotient)
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
index = 26
|
||||
} //Accounts for the edge case of the final letter; avoids getting an empty string
|
||||
|
||||
return result + indexLetter(index)
|
||||
}
|
||||
/**
|
||||
* A plugin to add printer pages
|
||||
* */
|
||||
export const pagesPlugin = ({ size = 'a4', ...settings }) => {
|
||||
const ls = settings.orientation === 'landscape'
|
||||
let sheetHeight = sizes[size][ls ? 0 : 1]
|
||||
let sheetWidth = sizes[size][ls ? 1 : 0]
|
||||
sheetWidth -= settings.margin * 2
|
||||
sheetHeight -= settings.margin * 2
|
||||
|
||||
return basePlugin({ ...settings, sheetWidth, sheetHeight })
|
||||
}
|
||||
|
||||
export const fabricPlugin = (settings) => {
|
||||
return basePlugin({
|
||||
...settings,
|
||||
partName: 'fabric',
|
||||
responsiveColumns: false,
|
||||
})
|
||||
}
|
||||
|
||||
/** check if there is anything to render on the given section of the svg so that we can skip empty pages */
|
||||
const doScanForBlanks = (stacks, layout, x, y, w, h) => {
|
||||
let hasContent = false
|
||||
for (var s in stacks) {
|
||||
let stack = stacks[s]
|
||||
|
||||
// get the position of the part
|
||||
let stackLayout = layout.stacks[s]
|
||||
if (!stackLayout) continue
|
||||
|
||||
let stackMinX = stackLayout.tl?.x || stackLayout.move.x + stack.topLeft.x
|
||||
let stackMinY = stackLayout.tl?.y || stackLayout.move.y + stack.topLeft.y
|
||||
let stackMaxX = stackLayout.br?.x || stackMinX + stack.width
|
||||
let stackMaxY = stackLayout.br?.y || stackMinY + stack.height
|
||||
|
||||
// check if the stack overlaps the page extents
|
||||
if (
|
||||
// if the left of the stack is further left than the right end of the page
|
||||
stackMinX < x + w &&
|
||||
// and the top of the stack is above the bottom of the page
|
||||
stackMinY < y + h &&
|
||||
// and the right of the stack is further right than the left of the page
|
||||
stackMaxX > x &&
|
||||
// and the bottom of the stack is below the top to the page
|
||||
stackMaxY > y
|
||||
) {
|
||||
// the stack has content inside the page
|
||||
hasContent = true
|
||||
// so we stop looking
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return hasContent
|
||||
}
|
||||
|
||||
export function addToOnly(pattern, partName) {
|
||||
const only = pattern.settings[0].only
|
||||
if (only && !only.includes(partName)) {
|
||||
pattern.settings[0].only = [].concat(only, partName)
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromOnly(pattern, partName) {
|
||||
const only = pattern.settings[0].only
|
||||
if (only && only.includes(partName)) {
|
||||
pattern.settings[0].only.splice(only.indexOf(partName), 1)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The base plugin for adding a layout helper part like pages or fabric
|
||||
* sheetWidth: the width of the helper part
|
||||
* sheetHeight: the height of the helper part
|
||||
* boundary: should the helper part calculate its boundary?
|
||||
* responsiveColumns: should the part make more columns if the pattern exceed its width? (for pages you want this, for fabric you don't)
|
||||
* printStyle: should the pages be rendered for printing or for screen viewing?
|
||||
* */
|
||||
const basePlugin = ({
|
||||
sheetWidth,
|
||||
sheetHeight,
|
||||
// boundary = false,
|
||||
partName = 'pages',
|
||||
responsiveColumns = true,
|
||||
printStyle = false,
|
||||
scanForBlanks = true,
|
||||
renderBlanks = true,
|
||||
setPatternSize = false,
|
||||
}) => ({
|
||||
name,
|
||||
version,
|
||||
hooks: {
|
||||
preDraft: function (pattern) {
|
||||
if (!responsiveColumns) {
|
||||
pattern.settings[0].maxWidth = sheetWidth
|
||||
}
|
||||
|
||||
addToOnly(pattern, partName)
|
||||
// Add part
|
||||
pattern.addPart({
|
||||
name: partName,
|
||||
draft: (shorthand) => {
|
||||
const layoutData = shorthand.store.get('layoutData')
|
||||
// only actually draft the part if layout data has been set
|
||||
if (layoutData) {
|
||||
shorthand.macro('addPages', layoutData, shorthand)
|
||||
shorthand.part.unhide()
|
||||
} else {
|
||||
shorthand.part.hide()
|
||||
}
|
||||
return shorthand.part
|
||||
},
|
||||
})
|
||||
},
|
||||
postLayout: function (pattern) {
|
||||
let { height, width, stacks } = pattern
|
||||
if (!responsiveColumns) width = sheetWidth
|
||||
// get the layout
|
||||
const layout =
|
||||
typeof pattern.settings[pattern.activeSet].layout === 'object'
|
||||
? pattern.settings[pattern.activeSet].layout
|
||||
: pattern.autoLayout
|
||||
|
||||
// if the layout doesn't start at 0,0 we want to account for that in our height and width
|
||||
if (layout?.topLeft) {
|
||||
height += layout.topLeft.y
|
||||
responsiveColumns && (width += layout.topLeft.x)
|
||||
}
|
||||
|
||||
// store the layout data so the part can use it during drafting
|
||||
pattern.setStores[pattern.activeSet].set('layoutData', {
|
||||
size: [sheetHeight, sheetWidth],
|
||||
height,
|
||||
width,
|
||||
layout,
|
||||
stacks,
|
||||
})
|
||||
|
||||
// draft the part
|
||||
pattern.draftPartForSet(partName, pattern.activeSet)
|
||||
|
||||
// if the pattern size is supposed to be re-set to the full width and height of all pages, do that
|
||||
const generatedPageData = pattern.setStores[pattern.activeSet].get(partName)
|
||||
if (setPatternSize === true || setPatternSize === 'width')
|
||||
pattern.width = Math.max(pattern.width, sheetWidth * generatedPageData.cols)
|
||||
if (setPatternSize === true || setPatternSize === 'height')
|
||||
pattern.height = Math.max(pattern.height, sheetHeight * generatedPageData.rows)
|
||||
|
||||
removeFromOnly(pattern, partName)
|
||||
},
|
||||
preRender: function (svg) {
|
||||
addToOnly(svg.pattern, partName)
|
||||
},
|
||||
postRender: function (svg) {
|
||||
removeFromOnly(svg.pattern, partName)
|
||||
},
|
||||
},
|
||||
macros: {
|
||||
banner: pluginAnnotations.macros.banner,
|
||||
/** draft the pages */
|
||||
addPages: function (so, shorthand) {
|
||||
const [h, w] = so.size
|
||||
const cols = Math.ceil(so.width / w)
|
||||
const rows = Math.ceil(so.height / h)
|
||||
const { points, Point, paths, Path, part, macro, store } = shorthand
|
||||
let count = 0
|
||||
let withContent = {}
|
||||
part.topLeft = so.layout.topLeft || { x: 0, y: 0 }
|
||||
|
||||
// get the layout from the pattern
|
||||
const { layout } = so
|
||||
for (let row = 0; row < rows; row++) {
|
||||
let y = row * h
|
||||
withContent[row] = {}
|
||||
for (let col = 0; col < cols; col++) {
|
||||
let x = col * w
|
||||
let hasContent = true
|
||||
if (scanForBlanks && layout) {
|
||||
hasContent = doScanForBlanks(so.stacks, layout, x, y, w, h)
|
||||
}
|
||||
withContent[row][col] = hasContent
|
||||
if (!renderBlanks && !hasContent) continue
|
||||
if (hasContent) count++
|
||||
const pageName = `_pages__row${row}-col${col}`
|
||||
points[`${pageName}-tl`] = new Point(x, y)
|
||||
points[`${pageName}-tr`] = new Point(x + w, y)
|
||||
points[`${pageName}-br`] = new Point(x + w, y + h)
|
||||
points[`${pageName}-bl`] = new Point(x, y + h)
|
||||
points[`${pageName}-circle`] = new Point(x + w / 2, y + h / 2)
|
||||
.setCircle(56, 'stroke-4xl muted fabric')
|
||||
.attr('data-circle-id', `${pageName}-circle`)
|
||||
points[`${pageName}-text`] = new Point(x + w / 2, y + h / 2)
|
||||
.setText(
|
||||
`${responsiveColumns ? indexStr(col + 1) : ''}${row + 1}`,
|
||||
'text-4xl center baseline-center bold muted fill-fabric'
|
||||
)
|
||||
.attr('data-text-id', `${pageName}-text`)
|
||||
|
||||
paths[pageName] = new Path()
|
||||
.attr('id', pageName)
|
||||
.move(points[`${pageName}-tl`])
|
||||
.line(points[`${pageName}-bl`])
|
||||
.line(points[`${pageName}-br`])
|
||||
.line(points[`${pageName}-tr`])
|
||||
.close()
|
||||
|
||||
// add an edge warning if it can't expand horizontally
|
||||
if (!responsiveColumns) {
|
||||
paths[pageName + '_edge'] = new Path()
|
||||
.move(points[`${pageName}-tr`])
|
||||
.line(points[`${pageName}-br`])
|
||||
// .move(points[`${pageName}-br`].translate(20, 0))
|
||||
.addClass('help contrast stroke-xl')
|
||||
|
||||
shorthand.macro('banner', {
|
||||
path: paths[pageName + '_edge'],
|
||||
text: 'plugin:edgeOf' + shorthand.utils.capitalize(partName),
|
||||
className: 'text-xl center',
|
||||
spaces: 20,
|
||||
})
|
||||
}
|
||||
|
||||
if (col === cols - 1 && row === rows - 1) {
|
||||
const br = points[`${pageName}-br`]
|
||||
part.width = br.x
|
||||
part.height = br.y
|
||||
part.bottomRight = { x: br.x, y: br.y }
|
||||
}
|
||||
|
||||
if (!printStyle) {
|
||||
paths[pageName]
|
||||
.attr('class', 'fill-fabric')
|
||||
.attr(
|
||||
'style',
|
||||
`stroke-opacity: 0; fill-opacity: ${(col + row) % 2 === 0 ? 0.03 : 0.15};`
|
||||
)
|
||||
} else {
|
||||
paths[pageName].attr('class', 'interfacing stroke-xs')
|
||||
// add markers and rulers
|
||||
macro('addPageMarkers', { row, col, pageName, withContent }, shorthand)
|
||||
macro('addRuler', { xAxis: true, pageName }, shorthand)
|
||||
macro('addRuler', { xAxis: false, pageName }, shorthand)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store page count in part
|
||||
store.set(partName, { cols, rows, count, withContent, width: w, height: h })
|
||||
},
|
||||
/** add a ruler to the top left corner of the page */
|
||||
addRuler({ xAxis, pageName }, shorthand) {
|
||||
const { points, paths, Path } = shorthand
|
||||
const isMetric = this.context.settings.units === 'metric'
|
||||
// not so arbitrary number of units for the ruler
|
||||
const rulerLength = isMetric ? 10 : 2
|
||||
// distance to the end of the ruler
|
||||
const endPointDist = [(isMetric ? 10 : 25.4) * rulerLength, 0]
|
||||
|
||||
const axisName = xAxis ? 'x' : 'y'
|
||||
const rulerName = `${pageName}-${axisName}`
|
||||
// start by making an endpoint for the ruler based on the axis
|
||||
const endPoint = [endPointDist[xAxis ? 0 : 1], endPointDist[xAxis ? 1 : 0]]
|
||||
points[`${rulerName}-ruler-end`] = points[`${pageName}-tl`].translate(
|
||||
endPoint[0],
|
||||
endPoint[1]
|
||||
)
|
||||
// also make a tick for the end of the ruler
|
||||
points[`${rulerName}-ruler-tick`] = points[`${rulerName}-ruler-end`]
|
||||
.translate(xAxis ? 0 : 3, xAxis ? 3 : 0)
|
||||
// add a label to it
|
||||
.attr('data-text', rulerLength + (isMetric ? 'cm' : '"'))
|
||||
// space the text properly from the end of the line
|
||||
.attr('data-text-class', 'fill-interfacing baseline-center' + (xAxis ? ' center' : ''))
|
||||
.attr(`data-text-d${xAxis ? 'y' : 'x'}`, xAxis ? 5 : 3)
|
||||
// give the text an explicit id in case we need to hide it later
|
||||
.attr('data-text-id', `${rulerName}-ruler-text`)
|
||||
|
||||
// start the path
|
||||
paths[`${rulerName}-ruler`] = new Path()
|
||||
.move(points[`${pageName}-tl`])
|
||||
// give it an explicit id in case we need to hide it later
|
||||
.attr('id', `${rulerName}-ruler`)
|
||||
.attr('class', 'interfacing stroke-xs')
|
||||
|
||||
// get the distance between the smaller ticks on the rule
|
||||
const division = (isMetric ? 0.1 : 0.125) / rulerLength
|
||||
// Set up intervals for whole and half units.
|
||||
const wholeInterval = isMetric ? 10 : 8
|
||||
const halfInterval = isMetric ? 5 : 4
|
||||
let tickCounter = 1
|
||||
// we're going to go by fraction, so we want to do this up to 1
|
||||
for (var d = division; d < 1; d += division) {
|
||||
// make a start point
|
||||
points[`${rulerName}-ruler-${d}-end`] = points[`${pageName}-tl`].shiftFractionTowards(
|
||||
points[`${rulerName}-ruler-end`],
|
||||
d
|
||||
)
|
||||
|
||||
// base tick size on whether this is a major interval or a minor one
|
||||
let tick = 1
|
||||
// if this tick indicates a whole unit, extra long
|
||||
if (tickCounter % wholeInterval === 0) tick = 3
|
||||
// if this tick indicates half a unit, long
|
||||
else if (tickCounter % halfInterval === 0) tick = 2
|
||||
tickCounter++
|
||||
|
||||
// make a point for the end of the tick
|
||||
points[`${rulerName}-ruler-${d}-tick`] = points[`${rulerName}-ruler-${d}-end`].translate(
|
||||
xAxis ? 0 : tick,
|
||||
xAxis ? tick : 0
|
||||
)
|
||||
|
||||
// add the whole set to the ruler path
|
||||
paths[`${rulerName}-ruler`]
|
||||
.line(points[`${rulerName}-ruler-${d}-end`])
|
||||
.line(points[`${rulerName}-ruler-${d}-tick`])
|
||||
.line(points[`${rulerName}-ruler-${d}-end`])
|
||||
}
|
||||
|
||||
// add the end
|
||||
paths[`${rulerName}-ruler`]
|
||||
.line(points[`${rulerName}-ruler-end`])
|
||||
.line(points[`${rulerName}-ruler-tick`])
|
||||
},
|
||||
/** add page markers to the given page */
|
||||
addPageMarkers({ row, col, pageName, withContent }, shorthand) {
|
||||
const { macro, points } = shorthand
|
||||
// these markers are placed on the top and left of the page,
|
||||
// so skip markers for the top row or leftmost column
|
||||
if (row !== 0 && withContent[row - 1][col])
|
||||
macro('addPageMarker', {
|
||||
along: [points[`${pageName}-tr`], points[`${pageName}-tl`]],
|
||||
label: [`${row}`, `${row + 1}`],
|
||||
isRow: true,
|
||||
pageName,
|
||||
})
|
||||
if (col !== 0 && withContent[row][col - 1])
|
||||
macro('addPageMarker', {
|
||||
along: [points[`${pageName}-tl`], points[`${pageName}-bl`]],
|
||||
label: [indexStr(col), indexStr(col + 1)],
|
||||
isRow: false,
|
||||
pageName,
|
||||
})
|
||||
},
|
||||
/** add a page marker for either the row or the column, to aid with alignment and orientation */
|
||||
addPageMarker({ along, label, isRow, pageName }) {
|
||||
const { points, paths, Path, scale } = this.shorthand()
|
||||
const markerName = `${pageName}-${isRow ? 'row' : 'col'}-marker`
|
||||
|
||||
// x and y distances between corners. one will always be 0, which is helpful
|
||||
const xDist = along[0].dx(along[1])
|
||||
const yDist = along[0].dy(along[1])
|
||||
|
||||
// size of the x mark should be impacted by the scale setting
|
||||
const markSize = 4 * scale
|
||||
|
||||
// make one at 25% and one at 75%
|
||||
for (var d = 25; d < 100; d += 50) {
|
||||
// get points to make an x at d% along the edge
|
||||
const dName = `${markerName}-${d}`
|
||||
const centerName = `${dName}-center`
|
||||
points[centerName] = along[0].translate((xDist * d) / 100, (yDist * d) / 100)
|
||||
points[`${dName}-tr`] = points[centerName].translate(-markSize, markSize)
|
||||
points[`${dName}-tl`] = points[centerName].translate(-markSize, -markSize)
|
||||
points[`${dName}-br`] = points[centerName].translate(markSize, markSize)
|
||||
points[`${dName}-bl`] = points[centerName].translate(markSize, -markSize)
|
||||
|
||||
// make a path for the x
|
||||
paths[`${dName}`] = new Path()
|
||||
.move(points[`${dName}-tr`])
|
||||
.line(points[`${dName}-bl`])
|
||||
.move(points[`${dName}-tl`])
|
||||
.line(points[`${dName}-br`])
|
||||
.attr('class', 'interfacing stroke-xs')
|
||||
// give it an explicit ID in case we need to hide it later
|
||||
.attr('id', dName)
|
||||
|
||||
// add directional labels
|
||||
let angle = along[0].angle(along[1]) - 90
|
||||
for (var i = 0; i < 2; i++) {
|
||||
points[`${dName}-label-${i + 1}`] = points[centerName]
|
||||
.shift(angle, markSize)
|
||||
.setText(label[i], 'text-xs center baseline-center fill-interfacing')
|
||||
// give it an explicit ID in case we need to hide it later
|
||||
.attr('data-text-id', `${dName}-text-${i + 1}`)
|
||||
// rotate for the next one
|
||||
angle += 180
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
|
@ -1,78 +0,0 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { PrintLayoutSettings } from './settings.mjs'
|
||||
import { Draft } from '../draft/index.mjs'
|
||||
import { pagesPlugin } from '../plugin-layout-part.mjs'
|
||||
import {
|
||||
handleExport,
|
||||
defaultPdfSettings,
|
||||
} from 'shared/components/workbench/exporting/export-handler.mjs'
|
||||
import { Popout } from 'shared/components/popout.mjs'
|
||||
|
||||
export const PrintLayout = (props) => {
|
||||
// disable xray
|
||||
useEffect(() => {
|
||||
if (props.gist?._state?.xray?.enabled) props.updateGist(['_state', 'xray', 'enabled'], false)
|
||||
})
|
||||
|
||||
const { t } = useTranslation(['workbench', 'plugin'])
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
const draft = props.draft
|
||||
|
||||
// add the pages plugin to the draft
|
||||
const layoutSettings = {
|
||||
...defaultPdfSettings,
|
||||
...props.gist?._state?.layout?.forPrinting?.page,
|
||||
}
|
||||
draft.use(pagesPlugin(layoutSettings))
|
||||
|
||||
let patternProps
|
||||
try {
|
||||
// draft the pattern
|
||||
draft.draft()
|
||||
patternProps = draft.getRenderProps()
|
||||
} catch (err) {
|
||||
console.log(err, props.gist)
|
||||
}
|
||||
const bgProps = { fill: 'none' }
|
||||
|
||||
const exportIt = () => {
|
||||
setError(false)
|
||||
handleExport(
|
||||
'pdf',
|
||||
props.gist,
|
||||
props.design,
|
||||
t,
|
||||
props.app,
|
||||
() => setError(false),
|
||||
() => setError(true)
|
||||
)
|
||||
}
|
||||
|
||||
let name = props.design.designConfig.data.name
|
||||
name = name.replace('@freesewing/', '')
|
||||
return (
|
||||
<div>
|
||||
<h2 className="capitalize">{t('layoutThing', { thing: name }) + ': ' + t('forPrinting')}</h2>
|
||||
<div className="m-4">
|
||||
<PrintLayoutSettings {...{ ...props, exportIt, layoutSettings }} draft={draft} />
|
||||
{error && (
|
||||
<Popout warning compact>
|
||||
<span className="font-bold mr-4 uppercase text-sm">{t('error')}:</span>
|
||||
{t('somethingWentWrong')}
|
||||
</Popout>
|
||||
)}
|
||||
</div>
|
||||
<Draft
|
||||
draft={draft}
|
||||
gist={props.gist}
|
||||
updateGist={props.updateGist}
|
||||
patternProps={patternProps}
|
||||
bgProps={bgProps}
|
||||
gistReady={props.gistReady}
|
||||
layoutPart="pages"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { PageIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const PageOrientationPicker = ({ gist, updateGist }) => {
|
||||
const { t } = useTranslation(['workbench'])
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
btn btn-primary flex flex-row gap-2 items-center
|
||||
hover:text-primary-content
|
||||
`}
|
||||
onClick={() =>
|
||||
updateGist(
|
||||
['_state', 'layout', 'forPrinting', 'page', 'orientation'],
|
||||
gist._state?.layout?.forPrinting?.page?.orientation === 'portrait'
|
||||
? 'landscape'
|
||||
: 'portrait'
|
||||
)
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
gist._state?.layout?.forPrinting?.page?.orientation === 'landscape' ? 'rotate-90' : ''
|
||||
}
|
||||
>
|
||||
<PageIcon />
|
||||
</span>
|
||||
<span>{t(`pageOrientation`)}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import { PageSizeIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { Popout } from 'shared/components/popout.mjs'
|
||||
|
||||
const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid']
|
||||
|
||||
export const PageSizePicker = ({ gist, updateGist }) => {
|
||||
const { t } = useTranslation(['workbench'])
|
||||
const setSize = (size) => {
|
||||
updateGist(['_state', 'layout', 'forPrinting', 'page', 'size'], size)
|
||||
if (!gist._state?.layout?.forPrinting?.page?.orientation) {
|
||||
updateGist(['_state', 'layout', 'forPrinting', 'page', 'orientation'], 'portrait')
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!gist._state?.layout?.forPrinting?.page?.size ||
|
||||
sizes.indexOf(gist._state.layout.forPrinting.page.size) === -1
|
||||
)
|
||||
return (
|
||||
<Popout tip>
|
||||
<h3>{t('startBySelectingAThing', { thing: t('pageSize') })}</h3>
|
||||
<div className="flex flex-row gap-4">
|
||||
{sizes.map((size) => (
|
||||
<button key={size} onClick={() => setSize(size)} className="btn btn-primary">
|
||||
<span className="capitalize">{size}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popout>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`dropdown`}>
|
||||
<div
|
||||
tabIndex="0"
|
||||
className={`
|
||||
m-0 btn btn-primary flex flex-row gap-2
|
||||
hover:text-primary-content
|
||||
|
||||
`}
|
||||
>
|
||||
<PageSizeIcon />
|
||||
<span>{t(`pageSize`)}:</span>
|
||||
<span className="ml-2 font-bold">{gist._state.layout.forPrinting.page.size}</span>
|
||||
</div>
|
||||
<ul tabIndex="0" className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52">
|
||||
{sizes.map((size) => (
|
||||
<li key={size}>
|
||||
<button onClick={() => setSize(size)} className="btn btn-ghost hover:bg-base-200">
|
||||
<span className="text-base-content capitalize">{size}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
import { PageSizePicker } from './pagesize-picker.mjs'
|
||||
import { PageOrientationPicker } from './orientation-picker.mjs'
|
||||
import { PrintIcon, RightIcon, ClearIcon, ExportIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ShowButtonsToggle } from '../draft/buttons.mjs'
|
||||
|
||||
export const PrintLayoutSettings = (props) => {
|
||||
const { t } = useTranslation(['workbench'])
|
||||
let pages = props.draft?.setStores[0].get('pages')
|
||||
if (!pages) return null
|
||||
const { cols, rows, count } = pages
|
||||
|
||||
const setMargin = (evt) => {
|
||||
props.updateGist(
|
||||
['_state', 'layout', 'forPrinting', 'page', 'margin'],
|
||||
parseInt(evt.target.value)
|
||||
)
|
||||
}
|
||||
|
||||
const setCoverPage = () => {
|
||||
props.updateGist(
|
||||
['_state', 'layout', 'forPrinting', 'page', 'coverPage'],
|
||||
!props.layoutSettings.coverPage
|
||||
)
|
||||
}
|
||||
|
||||
const setCutlist = () => {
|
||||
props.updateGist(
|
||||
['_state', 'layout', 'forPrinting', 'page', 'cutlist'],
|
||||
!props.layoutSettings.cutlist
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="flex flex-row justify-between
|
||||
mb-2"
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<PageSizePicker {...props} />
|
||||
<PageOrientationPicker {...props} />
|
||||
<button
|
||||
key="export"
|
||||
onClick={props.exportIt}
|
||||
className="btn btn-primary btn-outline"
|
||||
disabled={count === 0}
|
||||
aria-disabled={count === 0}
|
||||
>
|
||||
<ExportIcon className="h-6 w-6 mr-2" />
|
||||
{`${t('export')} PDF`}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<ShowButtonsToggle
|
||||
gist={props.gist}
|
||||
updateGist={props.updateGist}
|
||||
layoutSetType="forPrinting"
|
||||
></ShowButtonsToggle>
|
||||
<button
|
||||
key="reset"
|
||||
onClick={() => props.unsetGist(['layouts', 'printingLayout'])}
|
||||
className="btn btn-primary btn-outline"
|
||||
>
|
||||
<ClearIcon className="h-6 w-6 mr-2" />
|
||||
{t('reset')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row">
|
||||
<label htmlFor="pageMargin" className="label">
|
||||
<span className="">{t('pageMargin')}</span>
|
||||
<input
|
||||
type="range"
|
||||
max={50}
|
||||
min={0}
|
||||
step={1}
|
||||
onChange={setMargin}
|
||||
value={props.layoutSettings.margin}
|
||||
className="range range-sm mx-2"
|
||||
name="pageMargin"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<span className="text-secondary">{props.layoutSettings.margin}mm</span>
|
||||
</div>
|
||||
<button
|
||||
title={t('reset')}
|
||||
className="btn btn-ghost btn-xs text-accent mx-2"
|
||||
disabled={props.layoutSettings.margin == 10}
|
||||
onClick={() => setMargin({ target: { value: 10 } })}
|
||||
>
|
||||
<ClearIcon />
|
||||
</button>
|
||||
</label>
|
||||
<label htmlFor="coverPage" className="label">
|
||||
<span className="mr-2">{t('coverPage')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary"
|
||||
checked={props.layoutSettings.coverPage}
|
||||
onChange={setCoverPage}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="cutlist" className="label">
|
||||
<span className="mr-2">{t('cutlist')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary"
|
||||
checked={props.layoutSettings.cutlist}
|
||||
onChange={setCutlist}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-row font-bold items-center px-0 text-xl">
|
||||
<PrintIcon />
|
||||
<span className="ml-2">{count}</span>
|
||||
<span className="mx-6 opacity-50">|</span>
|
||||
<RightIcon />
|
||||
<span className="ml-2">{cols}</span>
|
||||
<span className="mx-6 opacity-50">|</span>
|
||||
<div className="rotate-90">
|
||||
<RightIcon />
|
||||
</div>
|
||||
<span className="text-xl ml-2">{rows}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Markdown from 'react-markdown'
|
||||
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
import { formatMm } from 'shared/utils.mjs'
|
||||
import { Tab, Tabs } from '../mdx/tabs.mjs'
|
||||
import { Tab, Tabs } from '../tabs.mjs'
|
||||
|
||||
export const Error = ({ err }) => {
|
||||
// Include the error name and message info if it isn't already at the top
|
||||
|
@ -27,9 +27,7 @@ export const Error = ({ err }) => {
|
|||
}
|
||||
|
||||
// Markdown wrapper to suppress creation of P tags
|
||||
const Md = ({ children }) => (
|
||||
<Markdown components={{ p: (props) => props.children }}>{children}</Markdown>
|
||||
)
|
||||
const Md = ({ md }) => <Mdx components={{ p: (props) => props.children }} md={md} />
|
||||
|
||||
const Log = ({ log, units }) => {
|
||||
if (Array.isArray(log)) {
|
||||
|
@ -43,9 +41,9 @@ const Log = ({ log, units }) => {
|
|||
)
|
||||
else return log.map((l) => <Log log={l} key={l} />)
|
||||
} else if (log.message) return <Error err={log} />
|
||||
else if (typeof log === 'string') return <Md>{log}</Md>
|
||||
else if (typeof log === 'string') return <Md md={log} />
|
||||
|
||||
return <Md>Unrecognized log: {JSON.stringify(log, null, 2)}</Md>
|
||||
return <Md md={`Unrecognized log: ${JSON.stringify(log, null, 2)}`} />
|
||||
}
|
||||
|
||||
export const LogGroup = ({ type = 'info', logs = [], units = 'metric' }) =>
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
import React, { useMemo, useEffect, useState } from 'react'
|
||||
import { MeasurementInput } from '../inputs/measurement.mjs'
|
||||
import { adult, doll, giant } from '@freesewing/models'
|
||||
import {
|
||||
CisFemaleIcon as WomenswearIcon,
|
||||
CisMaleIcon as MenswearIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { Setting } from '../menu/core-settings/setting.mjs'
|
||||
import { settings } from '../menu/core-settings/index.mjs'
|
||||
import { Tab, Tabs } from 'shared/components/mdx/tabs.mjs'
|
||||
|
||||
const groups = { adult, doll, giant }
|
||||
|
||||
const icons = {
|
||||
cisFemale: <WomenswearIcon />,
|
||||
cisMale: <MenswearIcon />,
|
||||
}
|
||||
|
||||
export const WorkbenchMeasurements = ({ app, design, gist, updateGist, gistReady }) => {
|
||||
const { t } = useTranslation(['app', 'cfp'])
|
||||
|
||||
// Method to handle measurement updates
|
||||
const updateMeasurements = (value, m = false) => {
|
||||
if (m === false) {
|
||||
// Set all measurements
|
||||
updateGist('measurements', value)
|
||||
} else {
|
||||
// Set one measurement
|
||||
updateGist(['measurements', m], value)
|
||||
}
|
||||
}
|
||||
|
||||
const [firstInvalid, setFirstInvalid] = useState(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!gistReady) {
|
||||
return
|
||||
}
|
||||
for (const m of design.patternConfig?.measurements || []) {
|
||||
if (!gist?.measurements?.[m]) {
|
||||
setFirstInvalid(m)
|
||||
return
|
||||
}
|
||||
|
||||
setFirstInvalid(undefined)
|
||||
}
|
||||
}, [gistReady])
|
||||
|
||||
// Save us some typing
|
||||
const inputProps = useMemo(() => ({ app, updateMeasurements, gist }), [app, gist])
|
||||
const shortname = design.designConfig.data.name.replace('@freesewing/', '')
|
||||
|
||||
return (
|
||||
<div className="m-auto max-w-2xl">
|
||||
<h1>
|
||||
<span className="capitalize mr-4 opacity-70">{shortname}:</span> {t('measurements')}
|
||||
</h1>
|
||||
<h2>{t('cfp:preloadMeasurements')}</h2>
|
||||
<Tabs tabs="Adults, Dolls, Giants">
|
||||
{Object.keys(groups).map((group) => (
|
||||
<Tab tabId={group} key={group}>
|
||||
{Object.keys(icons).map((type) => (
|
||||
<React.Fragment key={type}>
|
||||
<h4 className="mt-4">{t(type)}</h4>
|
||||
<ul className="flex flex-row flex-wrap gap-2">
|
||||
{Object.keys(groups[group][type]).map((m) => (
|
||||
<li key={`${m}-${type}-${group}`} className="">
|
||||
<button
|
||||
className="flex flex-row btn btn-outline"
|
||||
onClick={() => updateMeasurements(groups[group][type][m], false)}
|
||||
>
|
||||
{icons[type]}
|
||||
{group === 'adult' ? `${t('size')} ${m}` : `${m}%`}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<h2 className="mt-8">{t('cfp:enterMeasurements')}</h2>
|
||||
<div className="my-2 border p-4 rounded-lg shadow bg-base-200">
|
||||
<Setting
|
||||
key={'units'}
|
||||
setting={'units'}
|
||||
config={settings.units}
|
||||
updateGist={updateGist}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
{design.patternConfig.measurements.length > 0 && (
|
||||
<>
|
||||
<h3>{t('requiredMeasurements')}</h3>
|
||||
{design.patternConfig.measurements.map((m) => (
|
||||
<MeasurementInput key={m} m={m} focus={m == firstInvalid} {...inputProps} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{design.patternConfig.optionalMeasurements.length > 0 && (
|
||||
<>
|
||||
<h3>{t('optionalMeasurements')}</h3>
|
||||
{design.patternConfig.optionalMeasurements.map((m) => (
|
||||
<MeasurementInput key={m} m={m} {...inputProps} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { SecText, SumButton, Li, SumDiv, Deg } from 'shared/components/workbench/menu/index.mjs'
|
||||
|
||||
export const CoreSettingBool = (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const [value, setValue] = useState(props.gist[props.setting])
|
||||
|
||||
const toggle = () => {
|
||||
props.updateGist([props.setting], !value)
|
||||
setValue(!value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<SumButton onClick={toggle}>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span>{t(`settings:${props.setting}.t`)}</span>
|
||||
</SumDiv>
|
||||
<SecText>{t(value ? 'yes' : 'no')}</SecText>
|
||||
</SumButton>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { Deg } from 'shared/components/workbench/menu/index.mjs'
|
||||
|
||||
export const CoreSettingList = (props) => {
|
||||
const { t } = useTranslation(['settings'])
|
||||
const { dflt } = props
|
||||
const val = props.gist?.[props.setting]
|
||||
|
||||
const [value, setValue] = useState(val)
|
||||
|
||||
const handleChange = (newVal) => {
|
||||
if (newVal === dflt) reset()
|
||||
else {
|
||||
setValue(newVal)
|
||||
props.updateGist([props.setting], newVal)
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
setValue(props.dflt)
|
||||
props.updateGist([props.setting], props.dflt)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4 mx-6 border-l-2 pl-2">
|
||||
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
|
||||
{t(`settings:${props.setting}.d`)}
|
||||
</p>
|
||||
<div className="flex flex-row">
|
||||
<div className="grow">
|
||||
{props.list.map((entry) => (
|
||||
<button
|
||||
key={entry.key}
|
||||
onClick={() => handleChange(entry.key)}
|
||||
className={`
|
||||
mr-1 mb-1 text-left text-lg w-full hover:text-secondary-focus px-2
|
||||
${entry.key === value && 'font-bold text-secondary'}
|
||||
`}
|
||||
>
|
||||
<Deg />
|
||||
{entry.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { formatMm } from 'shared/utils.mjs'
|
||||
import { ClearIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const CoreSettingMm = (props) => {
|
||||
const { t } = useTranslation(['app', 'settings'])
|
||||
const { dflt, min, max } = props
|
||||
const val = props.gist?.[props.setting]
|
||||
|
||||
const [value, setValue] = useState(val)
|
||||
|
||||
const handleChange = (evt) => {
|
||||
const newVal = parseFloat(evt.target.value)
|
||||
|
||||
if (newVal === dflt) reset()
|
||||
else {
|
||||
setValue(newVal)
|
||||
props.updateGist([props.setting], newVal)
|
||||
}
|
||||
}
|
||||
const reset = () => {
|
||||
setValue(props.dflt)
|
||||
props.updateGist([props.setting], props.dflt)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4 mx-6 border-l-2 pl-2">
|
||||
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
|
||||
{t(`settings:${props.setting}.d`)}
|
||||
</p>
|
||||
<div className="flex flex-row justify-between">
|
||||
<span
|
||||
className="opacity-50"
|
||||
dangerouslySetInnerHTML={{ __html: formatMm(min, props.gist.units) }}
|
||||
/>
|
||||
<span
|
||||
className={`font-bold ${val === dflt ? 'text-secondary-focus' : 'text-accent'}`}
|
||||
dangerouslySetInnerHTML={{ __html: formatMm(val, props.gist.units) }}
|
||||
/>
|
||||
<span
|
||||
className="opacity-50"
|
||||
dangerouslySetInnerHTML={{ __html: formatMm(max, props.gist.units) }}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
max={max}
|
||||
min={min}
|
||||
step={0.1}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className={`
|
||||
range range-sm mt-1
|
||||
${val === dflt ? 'range-secondary' : 'range-accent'}
|
||||
`}
|
||||
/>
|
||||
<div className="flex flex-row justify-between">
|
||||
<span />
|
||||
<button
|
||||
title={t('reset')}
|
||||
className="btn btn-ghost btn-xs text-accent"
|
||||
disabled={val === dflt}
|
||||
onClick={reset}
|
||||
>
|
||||
<ClearIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { ClearIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const CoreSettingNr = (props) => {
|
||||
const { t } = useTranslation(['app', 'settings'])
|
||||
const { dflt, min, max } = props
|
||||
const val = props.gist?.[props.setting]
|
||||
|
||||
const [value, setValue] = useState(val)
|
||||
|
||||
const handleChange = (evt) => {
|
||||
const newVal = parseFloat(evt.target.value)
|
||||
|
||||
if (newVal === dflt) reset()
|
||||
else {
|
||||
setValue(newVal)
|
||||
props.updateGist([props.setting], newVal)
|
||||
}
|
||||
}
|
||||
const reset = () => {
|
||||
setValue(props.dflt)
|
||||
props.updateGist([props.setting], props.dflt)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4 mx-6 border-l-2 pl-2">
|
||||
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
|
||||
{t(`settings:${props.setting}.d`)}
|
||||
</p>
|
||||
<div className="flex flex-row justify-between">
|
||||
<span className="opacity-50">{min}</span>
|
||||
<span className={`font-bold ${val === dflt ? 'text-secondary-focus' : 'text-accent'}`}>
|
||||
{val}
|
||||
</span>
|
||||
<span className="opacity-50">{max}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
max={max}
|
||||
min={min}
|
||||
step={0.1}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className={`
|
||||
range range-sm mt-1
|
||||
${val === dflt ? 'range-secondary' : 'range-accent'}
|
||||
`}
|
||||
/>
|
||||
<div className="flex flex-row justify-between">
|
||||
<span />
|
||||
<button
|
||||
title={t('reset')}
|
||||
className="btn btn-ghost btn-xs text-accent"
|
||||
disabled={val === dflt}
|
||||
onClick={reset}
|
||||
>
|
||||
<ClearIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
import { ClearIcon } from 'shared/components/icons.mjs'
|
||||
import orderBy from 'lodash.orderby'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const CoreSettingOnly = (props) => {
|
||||
const { t } = useTranslation(['app', 'parts', 'settings'])
|
||||
const list = props.design.patternConfig.draftOrder
|
||||
const partNames = list.map((part) => ({ id: part, name: t(`parts:${part}`) }))
|
||||
|
||||
const togglePart = (part) => {
|
||||
const parts = props.gist.only || []
|
||||
const newParts = new Set(parts)
|
||||
if (newParts.has(part)) newParts.delete(part)
|
||||
else newParts.add(part)
|
||||
if (newParts.size < 1) reset()
|
||||
else props.updateGist(['only'], [...newParts])
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
props.unsetGist(['only'])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4 mx-6 border-l-2 pl-2">
|
||||
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
|
||||
{t(`settings:only.d`)}
|
||||
</p>
|
||||
<div className="flex flex-row">
|
||||
<div className="grow">
|
||||
{orderBy(partNames, ['name'], ['asc']).map((part) => (
|
||||
<button
|
||||
key={part.id}
|
||||
onClick={() => togglePart(part.id)}
|
||||
className={`
|
||||
mr-1 mb-1 text-left text-lg w-full hover:text-secondary-focus px-2
|
||||
${
|
||||
props.gist?.only &&
|
||||
props.gist.only.indexOf(part.id) !== -1 &&
|
||||
'font-bold text-secondary-focus'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
text-3xl mr-2 inline-block p-0 leading-3
|
||||
translate-y-3
|
||||
`}
|
||||
>
|
||||
<>°</>
|
||||
</span>
|
||||
{part.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse">
|
||||
<button
|
||||
title={t('reset')}
|
||||
className="btn btn-ghost btn-xs text-accent"
|
||||
disabled={!props.gist.only || props.gist.only.length < 1}
|
||||
onClick={reset}
|
||||
>
|
||||
<ClearIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { SecText, SumButton, Li, SumDiv, Deg } from 'shared/components/workbench/menu/index.mjs'
|
||||
|
||||
export const CoreSettingSaBool = (props) => {
|
||||
const { t } = useTranslation(['app', 'settings'])
|
||||
const [value, setValue] = useState(props.gist.saBool || false)
|
||||
|
||||
const toggle = () => {
|
||||
props.setGist({
|
||||
...props.gist,
|
||||
saBool: !value,
|
||||
sa: value ? 0 : props.gist.saMm,
|
||||
})
|
||||
setValue(!value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<SumButton onClick={toggle}>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span>{t('settings:sabool.t')}</span>
|
||||
</SumDiv>
|
||||
<SecText>{t(value ? 'yes' : 'no')}</SecText>
|
||||
</SumButton>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
import { Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import { CoreSettingList as ListSetting } from './core-setting-list.mjs'
|
||||
import { CoreSettingOnly as OnlySetting } from './core-setting-only.mjs'
|
||||
import { CoreSettingMm as MmSetting } from './core-setting-mm.mjs'
|
||||
import { CoreSettingNr as NrSetting } from './core-setting-nr.mjs'
|
||||
import { CoreSettingBool as BoolSetting } from './core-setting-bool.mjs'
|
||||
import { CoreSettingSaBool as SaBoolSetting } from './core-setting-sa-bool.mjs'
|
||||
import { CoreSettingSaMm as SaMmSetting } from './core-setting-sa-mm.mjs'
|
||||
import { formatMm } from 'shared/utils.mjs'
|
||||
import {
|
||||
SecText,
|
||||
Li,
|
||||
Details,
|
||||
Summary,
|
||||
SumDiv,
|
||||
Deg,
|
||||
} from 'shared/components/workbench/menu/index.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
const settings = {
|
||||
paperless: (props) => <SecText>{props.t(props.gist.paperless ? 'yes' : 'no')}</SecText>,
|
||||
complete: (props) => <SecText>{props.t(props.gist.complete ? 'yes' : 'no')}</SecText>,
|
||||
debug: (props) => <SecText>{props.t(props.gist.debug ? 'yes' : 'no')}</SecText>,
|
||||
locale: (props) => <SecText>{props.t(`i18n:${props.gist.locale}`)}</SecText>,
|
||||
units: (props) => <SecText>{props.t(`${props.gist.units}Units`)}</SecText>,
|
||||
margin: (props) => <SecText raw={formatMm(props.gist.margin, props.gist.units)} />,
|
||||
scale: (props) =>
|
||||
props.gist.scale === 1 ? (
|
||||
<SecText>{props.gist.scale}</SecText>
|
||||
) : (
|
||||
<span className="text-accent">{props.gist.scale}</span>
|
||||
),
|
||||
saMm: (props) => <SecText raw={formatMm(props.gist.saMm, props.gist.units)} />,
|
||||
renderer: (props) => <SecText>{props.config.titles[props.gist.renderer]}</SecText>,
|
||||
only: (props) =>
|
||||
props.gist?.only && props.gist.only.length > 0 ? (
|
||||
<SecText>{props.gist.only.length}</SecText>
|
||||
) : (
|
||||
<span className="text-secondary-focus">{props.t('default')}</span>
|
||||
),
|
||||
}
|
||||
|
||||
const inputs = {
|
||||
locale: (props) => (
|
||||
<ListSetting
|
||||
{...props}
|
||||
list={props.config.list.map((key) => ({
|
||||
key,
|
||||
title: props.t(`i18n:${key}`),
|
||||
}))}
|
||||
/>
|
||||
),
|
||||
units: (props) => (
|
||||
<ListSetting
|
||||
{...props}
|
||||
list={props.config.list.map((key) => ({
|
||||
key,
|
||||
title: props.t(`${key}Units`),
|
||||
}))}
|
||||
/>
|
||||
),
|
||||
margin: (props) => <MmSetting {...props} {...props.config} />,
|
||||
scale: (props) => <NrSetting {...props} {...props.config} />,
|
||||
saMm: (props) => <SaMmSetting {...props} {...props.config} />,
|
||||
renderer: (props) => (
|
||||
<ListSetting
|
||||
{...props}
|
||||
list={props.config.list.map((key) => ({
|
||||
key,
|
||||
title: props.config.titles[key],
|
||||
}))}
|
||||
/>
|
||||
),
|
||||
only: (props) => <OnlySetting {...props} />,
|
||||
}
|
||||
|
||||
export const Setting = (props) => {
|
||||
const { t } = useTranslation(['app', 'i18n', 'settings'])
|
||||
if (props.setting === 'saBool') return <SaBoolSetting {...props} {...props.config} />
|
||||
if (['paperless', 'complete', 'debug', 'xray'].indexOf(props.setting) !== -1)
|
||||
return <BoolSetting {...props} {...props.config} />
|
||||
|
||||
const Input = inputs[props.setting]
|
||||
const Value = settings[props.setting]
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
{props.setting === 'saMm' ? (
|
||||
<>
|
||||
<span>{t(`settings:sa.t`)}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{t(`settings:${props.setting}.t`)}</span>
|
||||
)}
|
||||
</SumDiv>
|
||||
<Value setting={props.setting} {...props} t={t} />
|
||||
<Chevron />
|
||||
</Summary>
|
||||
<Input {...props} t={t} />
|
||||
</Details>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import { OptionsIcon } from 'shared/components/icons.mjs'
|
||||
import { Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import { OptionGroup } from './option-group.mjs'
|
||||
import { OptionComponent } from './option.mjs'
|
||||
import { Ul, Details, TopSummary, TopSumTitle } from 'shared/components/workbench/menu/index.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { optionsMenuStructure } from 'shared/utils.mjs'
|
||||
|
||||
export const DesignOptions = (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const Option = props.Option ? props.Option : OptionComponent
|
||||
const optionsMenu = optionsMenuStructure(props.design.patternConfig.options)
|
||||
|
||||
return (
|
||||
<Details open>
|
||||
<TopSummary icon={<OptionsIcon />}>
|
||||
<TopSumTitle>{t('designOptions')}</TopSumTitle>
|
||||
<Chevron />
|
||||
</TopSummary>
|
||||
<Ul className="pl-5 list-inside">
|
||||
{Object.entries(optionsMenu).map(([group, options]) =>
|
||||
typeof options === 'string' ? (
|
||||
<Option {...props} type={options} option={group} key={group} />
|
||||
) : (
|
||||
<OptionGroup {...props} group={group} options={options} key={group} Option={Option} />
|
||||
)
|
||||
)}
|
||||
</Ul>
|
||||
</Details>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import { Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import { Li, Ul, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu/index.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const OptionGroup = (props) => {
|
||||
const { t } = useTranslation(['optiongroups'])
|
||||
const Option = props.Option
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span className="font-bold">{t(props.group)}</span>
|
||||
</SumDiv>
|
||||
<Chevron />
|
||||
</Summary>
|
||||
<Ul>
|
||||
{Object.entries(props.options).map(([option, type]) =>
|
||||
typeof type === 'string' ? (
|
||||
<Option {...props} type={type} option={option} key={option} />
|
||||
) : (
|
||||
<OptionGroup {...props} group={option} options={type} key={option} Option={Option} />
|
||||
)
|
||||
)}
|
||||
</Ul>
|
||||
</Details>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { DesignOptionPctDeg } from 'shared/components/workbench/inputs/design-option-pct-deg.mjs'
|
||||
import { DesignOptionCount } from 'shared/components/workbench/inputs/design-option-count.mjs'
|
||||
import { DesignOptionList } from 'shared/components/workbench/inputs/design-option-list.mjs'
|
||||
import { Popout } from 'shared/components/popout.mjs'
|
||||
|
||||
export const Tmp = () => <p>not yet</p>
|
||||
|
||||
export const inputs = {
|
||||
Pct: DesignOptionPctDeg,
|
||||
Count: DesignOptionCount,
|
||||
Deg: (props) => <DesignOptionPctDeg {...props} type="deg" />,
|
||||
List: DesignOptionList,
|
||||
Mm: () => (
|
||||
<Popout fixme compact>
|
||||
Mm options are deprecated. Please report this
|
||||
</Popout>
|
||||
),
|
||||
Constant: Tmp,
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import { formatMm, formatPercentage } from 'shared/utils.mjs'
|
||||
|
||||
export const values = {
|
||||
Pct: (props) => {
|
||||
const val =
|
||||
typeof props.gist?.options?.[props.option] === 'undefined'
|
||||
? props.design.patternConfig.options[props.option].pct / 100
|
||||
: props.gist.options[props.option]
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
val === props.design.patternConfig.options[props.option].pct / 100
|
||||
? 'text-secondary-focus'
|
||||
: 'text-accent'
|
||||
}
|
||||
>
|
||||
{formatPercentage(val)}
|
||||
{props.design.patternConfig.options[props.option]?.toAbs && props.gist.measurements
|
||||
? ' | ' +
|
||||
formatMm(props.design.patternConfig.options[props.option]?.toAbs(val, props.gist))
|
||||
: null}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
Bool: (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const dflt = props.design.patternConfig.options[props.option].bool
|
||||
let current = props.gist?.options?.[props.option]
|
||||
current = current === undefined ? dflt : current
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
dflt == current || typeof current === 'undefined' ? 'text-secondary-focus' : 'text-accent'
|
||||
}
|
||||
>
|
||||
{current ? t('yes') : t('no')}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
Count: (props) => {
|
||||
const dflt = props.design.patternConfig.options[props.option].count
|
||||
const current = props.gist?.options?.[props.option]
|
||||
return dflt == current || typeof current === 'undefined' ? (
|
||||
<span className="text-secondary-focus">{dflt}</span>
|
||||
) : (
|
||||
<span className="text-accent">{current}</span>
|
||||
)
|
||||
},
|
||||
List: (props) => {
|
||||
const dflt = props.design.patternConfig.options[props.option].dflt
|
||||
const current = props.gist?.options?.[props.option]
|
||||
const prefix = `${props.option}.o.`
|
||||
const translate = props.design.patternConfig.options[props.option]?.doNotTranslate
|
||||
? (input) => input
|
||||
: (input) => props.t(prefix + input)
|
||||
return dflt == current || typeof current === 'undefined' ? (
|
||||
<span className="text-secondary-focus">{translate(dflt)}</span>
|
||||
) : (
|
||||
<span className="text-accent">{translate(current)}</span>
|
||||
)
|
||||
},
|
||||
Deg: (props) => {
|
||||
const dflt = props.design.patternConfig.options[props.option].deg
|
||||
const current = props.gist?.options?.[props.option]
|
||||
return dflt == current || typeof current === 'undefined' ? (
|
||||
<span className="text-secondary-focus">{dflt}°</span>
|
||||
) : (
|
||||
<span className="text-accent">{current}°</span>
|
||||
)
|
||||
},
|
||||
Mm: () => {
|
||||
return <p>No mm val yet</p>
|
||||
},
|
||||
Constant: () => {
|
||||
return <p>No constant val yet</p>
|
||||
},
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import { Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import { optionType } from 'shared/utils.mjs'
|
||||
import {
|
||||
Li,
|
||||
Details,
|
||||
Summary,
|
||||
SumButton,
|
||||
SumDiv,
|
||||
Deg,
|
||||
} from 'shared/components/workbench/menu/index.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { values } from 'shared/components/workbench/menu/design-options/option-value.mjs'
|
||||
import { inputs } from 'shared/components/workbench/menu/design-options/option-input.mjs'
|
||||
import { capitalize } from 'shared/utils.mjs'
|
||||
|
||||
export const OptionComponent = (props) => {
|
||||
const { t } = useTranslation([`o_${props.design.designConfig.data.name}`])
|
||||
const opt = props.design.patternConfig.options[props.option]
|
||||
const type = optionType(opt)
|
||||
const Input = inputs[capitalize(type)]
|
||||
const Value = values[capitalize(type)]
|
||||
|
||||
try {
|
||||
const hide = opt.hide && opt.hide(props.gist)
|
||||
|
||||
if (hide) return null
|
||||
} catch (e) {
|
||||
console.warn(`error occurred in hide method for ${props.option}, so we'll just show it`, e)
|
||||
}
|
||||
|
||||
if (type === 'bool') {
|
||||
const toggleBoolean = () => {
|
||||
const dflt = opt.bool
|
||||
const current = props.gist?.options?.[props.option]
|
||||
const newVal = typeof current === 'undefined' ? !dflt : !current
|
||||
props.updateGist(['options', props.option], newVal)
|
||||
}
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<SumButton onClick={toggleBoolean}>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span>{t(`${props.option}.t`)}</span>
|
||||
</SumDiv>
|
||||
<Value type={type} {...props} t={t} />
|
||||
</SumButton>
|
||||
</Li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span>{t(`${props.option}.t`)}</span>
|
||||
</SumDiv>
|
||||
<Value type={type} {...props} t={t} />
|
||||
<Chevron w={6} m={3} />
|
||||
</Summary>
|
||||
<Input {...props} ot={t} />
|
||||
</Details>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
import { linkClasses } from 'shared/components/navigation/primary.mjs'
|
||||
import { ViewMenu } from './view.mjs'
|
||||
import { DesignOptions } from './design-options/index.mjs'
|
||||
import { CoreSettings } from './core-settings/index.mjs'
|
||||
import { XrayMenu } from './xray/index.mjs'
|
||||
import { TestDesignOptions } from './test-design-options/index.mjs'
|
||||
|
||||
export const Ul = (props) => <ul className="pl-5 list-inside">{props.children}</ul>
|
||||
export const Li = (props) => (
|
||||
<li className="flex flex-row hover:border-r-2 hover:border-r-secondary">{props.children}</li>
|
||||
)
|
||||
export const Details = (props) => (
|
||||
<details className="grow" open={props.open || false}>
|
||||
{props.children}
|
||||
</details>
|
||||
)
|
||||
export const Deg = () => (
|
||||
<span className="text-3xl inline-block p-0 leading-3 px-2 translate-y-3">°</span>
|
||||
)
|
||||
export const NoSumDiv = (props) => (
|
||||
<div
|
||||
className={`
|
||||
grow px-2 ml-2 border-l-2
|
||||
${linkClasses}
|
||||
hover:cursor-resize
|
||||
hover:border-secondary
|
||||
sm:hover:border-secondary-focus
|
||||
text-base-content sm:text-base-content
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
export const SumDiv = (props) => (
|
||||
<div
|
||||
className={`
|
||||
grow pl-2 border-l-2
|
||||
${linkClasses}
|
||||
hover:cursor-resize
|
||||
hover:border-secondary
|
||||
sm:hover:border-secondary-focus
|
||||
text-base-content sm:text-base-content
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
export const Summary = (props) => (
|
||||
<summary
|
||||
className={`
|
||||
flex flex-row
|
||||
px-2
|
||||
text-base-content
|
||||
sm:text-base-content
|
||||
hover:cursor-row-resize
|
||||
items-center
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
</summary>
|
||||
)
|
||||
export const TopSummary = (props) => (
|
||||
<summary
|
||||
className={`
|
||||
flex flex-row gap-4 text-lg
|
||||
hover:cursor-row-resize
|
||||
p-2
|
||||
text-base-content
|
||||
sm:text-base-content
|
||||
items-center
|
||||
`}
|
||||
>
|
||||
<span className="text-secondary-focus mr-4">{props.icon || null}</span>
|
||||
{props.children}
|
||||
</summary>
|
||||
)
|
||||
export const SumButton = (props) => (
|
||||
<button
|
||||
className={`
|
||||
flex flex-row
|
||||
px-2
|
||||
w-full justify-between
|
||||
text-left
|
||||
text-base-content
|
||||
sm:text-base-content
|
||||
hover:cursor-pointer
|
||||
items-center
|
||||
mr-4
|
||||
`}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
export const TopSumTitle = (props) => (
|
||||
<span className={`grow ${linkClasses} hover:cursor-resize font-bold uppercase`}>
|
||||
{props.children}
|
||||
</span>
|
||||
)
|
||||
export const SecText = (props) =>
|
||||
props.raw ? (
|
||||
<span className="text-secondary-focus" dangerouslySetInnerHTML={{ __html: props.raw }} />
|
||||
) : (
|
||||
<span className="text-secondary-focus">{props.children}</span>
|
||||
)
|
||||
|
||||
export const WorkbenchMenu = (props) => {
|
||||
return (
|
||||
<nav className="grow mb-12">
|
||||
<ViewMenu {...props} />
|
||||
{['draft', 'cuttingLayout', 'printingLayout'].indexOf(props.gist?._state?.view) > -1 && (
|
||||
<>
|
||||
<DesignOptions {...props} />
|
||||
<CoreSettings {...props} />
|
||||
{props.gist.renderer === 'react' && <XrayMenu {...props} />}
|
||||
</>
|
||||
)}
|
||||
{props.gist?._state?.view === 'test' && <TestDesignOptions {...props} />}
|
||||
</nav>
|
||||
)
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
import { OptionsIcon } from 'shared/components/icons.mjs'
|
||||
import { Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import { OptionGroup } from '../design-options/option-group.mjs'
|
||||
import { Option } from './option.mjs'
|
||||
import { Ul, Details, TopSummary, TopSumTitle } from 'shared/components/workbench/menu/index.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { optionsMenuStructure } from 'shared/utils.mjs'
|
||||
import { adult, doll, giant } from '@freesewing/models'
|
||||
|
||||
const groups = { adult, doll, giant }
|
||||
|
||||
const SampleDesignOption = (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
|
||||
return (
|
||||
<Option
|
||||
updateGist={props.updateGist}
|
||||
option={props.option}
|
||||
design={props.design}
|
||||
active={props.gist.sample?.option}
|
||||
label={t(`o_${props.design.designConfig.data.name}:${props.option}.t`)}
|
||||
sampleSettings={{ type: 'option', option: props.option }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const TestDesignOptions = (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const optionsMenu = optionsMenuStructure(props.design.patternConfig.options)
|
||||
|
||||
const measies = props.draft?.config?.measurements || []
|
||||
|
||||
return (
|
||||
<>
|
||||
<Details open>
|
||||
<TopSummary icon={<OptionsIcon />}>
|
||||
<TopSumTitle>{t('designOptions')}</TopSumTitle>
|
||||
<Chevron />
|
||||
</TopSummary>
|
||||
<Ul className="pl-5 list-inside">
|
||||
{Object.entries(optionsMenu).map(([group, options]) =>
|
||||
typeof options === 'string' ? (
|
||||
<SampleDesignOption
|
||||
{...props}
|
||||
type={options}
|
||||
option={group}
|
||||
key={group}
|
||||
sampleSettings={{ type: 'option', options }}
|
||||
/>
|
||||
) : (
|
||||
<OptionGroup
|
||||
{...props}
|
||||
group={group}
|
||||
options={options}
|
||||
key={group}
|
||||
Option={SampleDesignOption}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Ul>
|
||||
</Details>
|
||||
|
||||
<Details open>
|
||||
<TopSummary icon={<OptionsIcon />}>
|
||||
<TopSumTitle>{t('measurements')}</TopSumTitle>
|
||||
<Chevron />
|
||||
</TopSummary>
|
||||
<Ul className="pl-5 list-inside">
|
||||
{measies.map((m) => (
|
||||
<Option
|
||||
updateGist={props.updateGist}
|
||||
option={m}
|
||||
design={props.design}
|
||||
active={props.gist.sample?.option}
|
||||
key={m}
|
||||
label={m}
|
||||
sampleSettings={{ type: 'measurement', measurement: m }}
|
||||
/>
|
||||
))}
|
||||
</Ul>
|
||||
</Details>
|
||||
|
||||
<Details open>
|
||||
<TopSummary icon={<OptionsIcon />}>
|
||||
<TopSumTitle>{t('models')}</TopSumTitle>
|
||||
<Chevron />
|
||||
</TopSummary>
|
||||
<Ul className="pl-5 list-inside">
|
||||
{Object.entries(groups).map(([group, modelGroups]) =>
|
||||
Object.entries(modelGroups).map(([name, models]) => (
|
||||
<Option
|
||||
updateGist={props.updateGist}
|
||||
label={`${group} - ${name}`}
|
||||
design={props.design}
|
||||
active={props.gist.sample?.option}
|
||||
key={name}
|
||||
sampleSettings={{ type: 'models', models }}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Ul>
|
||||
</Details>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import { Li, SumButton, SumDiv } from 'shared/components/workbench/menu/index.mjs'
|
||||
|
||||
export const Option = (props) => {
|
||||
const active = props.sampleSettings?.type === 'option' && props.active === props.option
|
||||
|
||||
const setSampleSettings = () => {
|
||||
props.updateGist(
|
||||
['sample'],
|
||||
props.sampleSettings,
|
||||
true // Close navigation on mobile
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<SumButton onClick={setSampleSettings}>
|
||||
<SumDiv active={active}>
|
||||
<span
|
||||
className={`
|
||||
text-3xl inline-block p-0 leading-3 px-2
|
||||
${
|
||||
active
|
||||
? 'text-secondary sm:text-secondary-focus translate-y-1 font-bold'
|
||||
: 'translate-y-3'
|
||||
}`}
|
||||
>
|
||||
{active ? <span>•</span> : <span>°</span>}
|
||||
</span>
|
||||
<span className={active ? 'text-secondary font-bold' : ''}>{props.label}</span>
|
||||
</SumDiv>
|
||||
</SumButton>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
import { MenuIcon } from 'shared/components/icons.mjs'
|
||||
import { linkClasses, Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { defaultGist } from 'shared/hooks/useGist.mjs'
|
||||
|
||||
export const ViewMenu = (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const entries = [
|
||||
{
|
||||
name: 'measurements',
|
||||
title: t('measurements'),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'measurements', true),
|
||||
},
|
||||
{
|
||||
name: 'draft',
|
||||
title: t('draftDesign', { design: props.design.designConfig.data.name }),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'draft', true),
|
||||
},
|
||||
{
|
||||
name: 'test',
|
||||
title: t('testDesign', { design: props.design.designConfig.data.name }),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'test', true),
|
||||
},
|
||||
{
|
||||
name: 'printingLayout',
|
||||
title:
|
||||
t('layoutThing', { thing: props.design.designConfig.data.name }) + ': ' + t('forPrinting'),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'printingLayout', true),
|
||||
},
|
||||
{
|
||||
name: 'cuttingLayout',
|
||||
title:
|
||||
t('layoutThing', { thing: props.design.designConfig.data.name }) + ': ' + t('forCutting'),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'cuttingLayout', true),
|
||||
},
|
||||
{
|
||||
name: 'export',
|
||||
title: t('exportThing', { thing: props.design.designConfig.data.name }),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'export', true),
|
||||
},
|
||||
{
|
||||
name: 'logs',
|
||||
title: t('logs'),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'logs', true),
|
||||
},
|
||||
{
|
||||
name: 'yaml',
|
||||
title: t('YAML'),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'yaml', true),
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
title: t('JSON'),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'json', true),
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
title: t('editThing', { thing: 'YAML' }),
|
||||
onClick: () => props.updateGist(['_state', 'view'], 'edit', true),
|
||||
},
|
||||
{
|
||||
name: 'clear',
|
||||
title: t('clearThing', { thing: 'YAML' }),
|
||||
onClick: () => props.setGist(defaultGist(props.design, props.gist.locale)),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<details className="py-1" open>
|
||||
<summary
|
||||
className={`
|
||||
flex flex-row uppercase gap-4 font-bold text-lg
|
||||
hover:cursor-row-resize
|
||||
p-2
|
||||
text-base-content
|
||||
sm:text-base-content
|
||||
items-center
|
||||
`}
|
||||
>
|
||||
<span className="text-secondary-focus mr-4">
|
||||
<MenuIcon />
|
||||
</span>
|
||||
<span className={`grow ${linkClasses} hover:cursor-resize`}>{t('view')}</span>
|
||||
<Chevron />
|
||||
</summary>
|
||||
<ul className="pl-5 list-inside">
|
||||
{entries.map((entry) => (
|
||||
<li key={entry.title} className="flex flex-row">
|
||||
<button
|
||||
title={entry.title}
|
||||
className={`
|
||||
grow pl-2 border-l-2
|
||||
${linkClasses}
|
||||
hover:cursor-pointer
|
||||
hover:border-secondary
|
||||
sm:hover:border-secondary-focus
|
||||
text-left
|
||||
capitalize
|
||||
${
|
||||
entry.name === props.gist?._state?.view
|
||||
? 'text-secondary border-secondary sm:text-secondary-focus sm:border-secondary-focus'
|
||||
: 'text-base-content sm:text-base-content'
|
||||
}
|
||||
`}
|
||||
onClick={entry.onClick}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
text-3xl mr-2 inline-block p-0 leading-3
|
||||
${
|
||||
entry.name === props.gist?._state?.view
|
||||
? 'text-secondary sm:text-secondary-focus translate-y-1 font-bold'
|
||||
: 'translate-y-3'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{entry.name === props.gist?._state?.view ? <>•</> : <>°</>}
|
||||
</span>
|
||||
<span className={entry.name === props.gist?._state?.view ? 'font-bold' : ''}>
|
||||
{entry.title}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { Chevron } from 'shared/components/navigation/primary'
|
||||
import { Ul, Li, Details, Summary, SumDiv, NoSumDiv, Deg } from 'shared/components/workbench/menu'
|
||||
|
||||
export const XrayAttributes = ({ attr = false, t }) => {
|
||||
if (!attr || !attr.list || Object.keys(attr.list).length < 1) return null
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
Attributes
|
||||
</SumDiv>
|
||||
<Chevron />
|
||||
</Summary>
|
||||
<Ul>
|
||||
{Object.keys(attr.list).map((at) => (
|
||||
<Li key={at}>
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
{at}
|
||||
</SumDiv>
|
||||
<Chevron />
|
||||
</Summary>
|
||||
<Ul>
|
||||
{attr.list[at].map((val) => (
|
||||
<Li key={val}>
|
||||
<NoSumDiv>
|
||||
<Deg />
|
||||
<span>{val === true ? t('app.yes') : val}</span>
|
||||
</NoSumDiv>
|
||||
</Li>
|
||||
))}
|
||||
</Ul>
|
||||
</Details>
|
||||
</Li>
|
||||
))}
|
||||
</Ul>
|
||||
</Details>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { Li, SumButton, SumDiv, Deg } from 'shared/components/workbench/menu'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const XrayDisable = (props) => {
|
||||
const { t } = useTranslation(['cfp', 'settings'])
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<SumButton onClick={() => props.updateGist(['_state', 'xray', 'enabled'], false)}>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span>{t('cfp:thingIsEnabled', { thing: t('settings:xray.t') })}</span>
|
||||
</SumDiv>
|
||||
</SumButton>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import { XrayIcon } from 'shared/components/icons.mjs'
|
||||
import { linkClasses, Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import { ConsoleLog } from './log.mjs'
|
||||
import { XrayReset } from './reset.mjs'
|
||||
import { XrayDisable } from './disable.mjs'
|
||||
import { XrayList } from './list.mjs'
|
||||
import { Ul, Details, TopSummary } from 'shared/components/workbench/menu'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const XrayMenu = (props) => {
|
||||
const { t } = useTranslation(['app', 'settings'])
|
||||
|
||||
return (
|
||||
<Details open>
|
||||
<TopSummary icon={<XrayIcon />}>
|
||||
{props.gist?._state?.xray?.enabled ? (
|
||||
<>
|
||||
<span className={`grow ${linkClasses} hover:cursor-resize font-bold uppercase`}>
|
||||
{t('settings:xray.t')}
|
||||
</span>
|
||||
<Chevron />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`grow ${linkClasses} hover:cursor-resize uppercase font-bold text-left`}
|
||||
onClick={() => props.updateGist(['_state', 'xray', 'enabled'], true)}
|
||||
>
|
||||
{t('settings:xray.t')}
|
||||
</button>
|
||||
<span className="text-normal text-secondary">
|
||||
{t('cfp:thingIsDisabled', { thing: t('settings:xray.t') })}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</TopSummary>
|
||||
{props.gist?._state?.xray?.enabled && (
|
||||
<Ul>
|
||||
<XrayDisable {...props} />
|
||||
<ConsoleLog {...props} />
|
||||
<XrayReset {...props} />
|
||||
{props.gist?._state?.xray?.parts &&
|
||||
Object.keys(props.gist._state.xray.parts).map((partName) => (
|
||||
<XrayList {...props} partName={partName} key={partName} />
|
||||
))}
|
||||
</Ul>
|
||||
)}
|
||||
</Details>
|
||||
)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import { Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import {
|
||||
Ul,
|
||||
Li,
|
||||
Details,
|
||||
Summary,
|
||||
SumButton,
|
||||
SumDiv,
|
||||
Deg,
|
||||
} from 'shared/components/workbench/menu/index.mjs'
|
||||
|
||||
export const ConsoleLog = (props) => (
|
||||
<Li>
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span>console.log()</span>
|
||||
</SumDiv>
|
||||
<Chevron />
|
||||
</Summary>
|
||||
<Ul>
|
||||
{['designConfig', 'patternConfig', 'gist', 'draft', 'renderProps'].map((it) => (
|
||||
<Li key={it}>
|
||||
<SumButton
|
||||
onClick={() => {
|
||||
if (it === 'designConfig') return console.log(props.design.designConfig)
|
||||
if (it === 'patternConfig') return console.log(props.design.patternConfig)
|
||||
if (it === 'renderProps') return console.log(props.draft.getRenderProps())
|
||||
return console.log(props[it])
|
||||
}}
|
||||
>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span>{it}</span>
|
||||
</SumDiv>
|
||||
</SumButton>
|
||||
</Li>
|
||||
))}
|
||||
</Ul>
|
||||
</Details>
|
||||
</Li>
|
||||
)
|
|
@ -1,66 +0,0 @@
|
|||
import { Chevron } from 'shared/components/navigation/primary'
|
||||
import { Ul, Li, Details, Summary, SumDiv, NoSumDiv, Deg } from 'shared/components/workbench/menu'
|
||||
import { XrayPoint } from './point'
|
||||
|
||||
const MoveLine = ({ op }) => <XrayPoint point={op.to} />
|
||||
const Curve = ({ op }) =>
|
||||
['cp1', 'cp2', 'to'].map((pnt) => (
|
||||
<Li key={pnt}>
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span className="font-bold">{pnt}</span>
|
||||
</SumDiv>
|
||||
<Chevron />
|
||||
</Summary>
|
||||
<XrayPoint point={op[pnt]} />
|
||||
</Details>
|
||||
</Li>
|
||||
))
|
||||
|
||||
const XrayPathOp = ({ op }) => (
|
||||
<Li>
|
||||
{op.type === 'close' ? (
|
||||
<NoSumDiv>
|
||||
<Deg />
|
||||
<span className="font-bold">{op.type}</span>
|
||||
</NoSumDiv>
|
||||
) : (
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span className="font-bold">{op.type}</span>
|
||||
<span>To</span>
|
||||
</SumDiv>
|
||||
<Chevron />
|
||||
</Summary>
|
||||
<Ul>{op.type === 'curve' ? <Curve op={op} /> : <MoveLine op={op} />}</Ul>
|
||||
</Details>
|
||||
)}
|
||||
</Li>
|
||||
)
|
||||
|
||||
export const XrayPathOps = ({ ops = false }) => {
|
||||
if (!ops || ops.length < 1) return null
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<Details>
|
||||
<Summary>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span className="font-bold">path.ops</span>
|
||||
</SumDiv>
|
||||
<Chevron />
|
||||
</Summary>
|
||||
<Ul>
|
||||
{ops.map((op) => (
|
||||
<XrayPathOp op={op} key={op} />
|
||||
))}
|
||||
</Ul>
|
||||
</Details>
|
||||
</Li>
|
||||
)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import { Ul, Li, NoSumDiv, Deg } from 'shared/components/workbench/menu/index.mjs'
|
||||
import { formatMm } from 'shared/utils.mjs'
|
||||
import { XrayAttributes } from './attributes.mjs'
|
||||
import { XrayPathOps } from './path-ops.mjs'
|
||||
|
||||
export const XrayPath = ({ pathName, partName, draft, units }) => {
|
||||
const path = draft?.parts?.[partName]?.paths?.[pathName]
|
||||
|
||||
if (!path) return null
|
||||
return (
|
||||
<Ul>
|
||||
<XrayAttributes attr={path.attributes} />
|
||||
<Li>
|
||||
<NoSumDiv>
|
||||
<Deg />
|
||||
<span className="font-bold mr-2">path.render =</span>
|
||||
<span>{JSON.stringify(path.render)}</span>
|
||||
</NoSumDiv>
|
||||
</Li>
|
||||
<Li>
|
||||
<NoSumDiv>
|
||||
<Deg />
|
||||
<span className="font-bold mr-2">path.length() =</span>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatMm(path.length(), units),
|
||||
}}
|
||||
/>
|
||||
</NoSumDiv>
|
||||
</Li>
|
||||
<XrayPathOps ops={path.ops} />
|
||||
</Ul>
|
||||
)
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { Ul, Li, NoSumDiv, Deg } from 'shared/components/workbench/menu'
|
||||
import { round } from 'shared/utils'
|
||||
import { XrayAttributes } from './attributes'
|
||||
|
||||
export const XrayPoint = ({ pointName, partName, draft, t }) => {
|
||||
const point = draft?.parts?.[partName]?.points?.[pointName]
|
||||
|
||||
return point ? (
|
||||
<Ul>
|
||||
{['x', 'y'].map((coord) => (
|
||||
<Li key={coord}>
|
||||
<NoSumDiv>
|
||||
<Deg />
|
||||
<span className="font-bold mr-2">{coord} =</span>
|
||||
<span>{round(point[coord])}</span>
|
||||
</NoSumDiv>
|
||||
</Li>
|
||||
))}
|
||||
<XrayAttributes attr={point.attributes} t={t} />
|
||||
</Ul>
|
||||
) : null
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { Li, SumButton, SumDiv, Deg } from 'shared/components/workbench/menu'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const XrayReset = (props) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
|
||||
return (
|
||||
<Li>
|
||||
<SumButton onClick={() => props.updateGist(['_state', 'xray'], { enabled: true })}>
|
||||
<SumDiv>
|
||||
<Deg />
|
||||
<span>{t(`reset`)}</span>
|
||||
</SumDiv>
|
||||
</SumButton>
|
||||
</Li>
|
||||
)
|
||||
}
|
148
sites/shared/components/workbench/menus/core-settings/config.mjs
Normal file
148
sites/shared/components/workbench/menus/core-settings/config.mjs
Normal file
|
@ -0,0 +1,148 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { measurementAsMm } from 'shared/utils.mjs'
|
||||
import {
|
||||
SaIcon,
|
||||
ScaleIcon,
|
||||
PaperlessIcon,
|
||||
I18nIcon,
|
||||
UnitsIcon,
|
||||
DetailIcon,
|
||||
IncludeIcon,
|
||||
MarginIcon,
|
||||
ExpandIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { controlLevels } from 'shared/config/freesewing.config.mjs'
|
||||
|
||||
export const defaultSamm = (units, inMm = true) => {
|
||||
const dflt = units === 'imperial' ? 0.5 : 1
|
||||
return inMm ? measurementAsMm(dflt, units) : dflt
|
||||
}
|
||||
|
||||
export const loadSettingsConfig = ({
|
||||
language = 'en',
|
||||
units = 'metric',
|
||||
sabool = false,
|
||||
parts = [],
|
||||
}) => ({
|
||||
sabool: {
|
||||
control: controlLevels.core.sa,
|
||||
list: [0, 1],
|
||||
choiceTitles: {
|
||||
0: 'saNo',
|
||||
1: 'saYes',
|
||||
},
|
||||
valueTitles: {
|
||||
0: 'no',
|
||||
1: 'yes',
|
||||
},
|
||||
dflt: 0,
|
||||
icon: SaIcon,
|
||||
},
|
||||
samm: sabool
|
||||
? {
|
||||
control: controlLevels.core.sa,
|
||||
min: 0,
|
||||
max: units === 'imperial' ? 2 : 2.5,
|
||||
dflt: defaultSamm(units),
|
||||
icon: SaIcon,
|
||||
}
|
||||
: false,
|
||||
paperless: {
|
||||
control: controlLevels.core.paperless,
|
||||
list: [0, 1],
|
||||
choiceTitles: {
|
||||
0: 'paperlessNo',
|
||||
1: 'paperlessYes',
|
||||
},
|
||||
valueTitles: {
|
||||
0: 'no',
|
||||
1: 'yes',
|
||||
},
|
||||
dflt: 0,
|
||||
icon: PaperlessIcon,
|
||||
},
|
||||
locale: {
|
||||
control: controlLevels.core.locale,
|
||||
list: ['de', 'en', 'es', 'fr', 'nl'],
|
||||
dflt: language,
|
||||
choiceTitles: {
|
||||
de: 'de',
|
||||
en: 'en',
|
||||
es: 'es',
|
||||
fr: 'fr',
|
||||
nl: 'nl',
|
||||
},
|
||||
valueTitles: {
|
||||
de: 'de.t',
|
||||
en: 'en.t',
|
||||
es: 'es.t',
|
||||
fr: 'fr.t',
|
||||
nl: 'nl.t',
|
||||
},
|
||||
icon: I18nIcon,
|
||||
},
|
||||
units: {
|
||||
control: controlLevels.core.units,
|
||||
list: ['metric', 'imperial'],
|
||||
dflt: 'metric',
|
||||
choiceTitles: {
|
||||
metric: 'metric',
|
||||
imperial: 'imperial',
|
||||
},
|
||||
valueTitles: {
|
||||
metric: 'metric',
|
||||
imperial: 'imperial',
|
||||
},
|
||||
icon: UnitsIcon,
|
||||
},
|
||||
complete: {
|
||||
control: controlLevels.core.complete,
|
||||
list: [1, 0],
|
||||
dflt: 1,
|
||||
choiceTitles: {
|
||||
0: 'completeNo',
|
||||
1: 'completeYes',
|
||||
},
|
||||
valueTitles: {
|
||||
0: 'no',
|
||||
1: 'yes',
|
||||
},
|
||||
icon: DetailIcon,
|
||||
},
|
||||
expand: {
|
||||
control: controlLevels.core.expand,
|
||||
list: [1, 0],
|
||||
dflt: 1,
|
||||
choiceTitles: {
|
||||
0: 'expandNo',
|
||||
1: 'expandYes',
|
||||
},
|
||||
valueTitles: {
|
||||
0: 'no',
|
||||
1: 'yes',
|
||||
},
|
||||
icon: ExpandIcon,
|
||||
},
|
||||
only: {
|
||||
control: controlLevels.core.only,
|
||||
dflt: false,
|
||||
list: parts,
|
||||
parts,
|
||||
icon: IncludeIcon,
|
||||
},
|
||||
scale: {
|
||||
control: controlLevels.core.scale,
|
||||
min: 0.1,
|
||||
max: 5,
|
||||
dflt: 1,
|
||||
step: 0.1,
|
||||
icon: ScaleIcon,
|
||||
},
|
||||
margin: {
|
||||
control: controlLevels.core.margin,
|
||||
min: 0,
|
||||
max: 2.5,
|
||||
dflt: measurementAsMm(units === 'imperial' ? 0.125 : 0.2, units),
|
||||
icon: MarginIcon,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
coreSettings.t: Kerneinstellungen
|
||||
coreSettings.d: Diese Einstellungen sind nicht spezifisch für das Design, sondern ermöglichen es dir, verschiedene Parameter der FreeSewing-Kernbibliothek anzupassen, die das Design für dich erstellt.
|
||||
designOptions.t: Diese Optionen sind spezifisch für dieses Muster. Du kannst sie verwenden, um dein Muster auf verschiedene Weise anzupassen.
|
||||
paperless.t: Papierlos
|
||||
paperless.d: Bäume sind toll und das Zusammenkleben von Nähmustern macht nicht viel Spaß. Probiere unseren papierlosen Modus aus, damit du dein Schnittmuster gar nicht erst ausdrucken musst.
|
||||
samm.t: Nahtzugabe Größe
|
||||
samm.d: Steuert die Menge der Nahtzugabe, die in deinem Muster verwendet wird
|
||||
sabool.t: Nahtzugabe einbeziehen
|
||||
sabool.d: Legt fest, ob eine Nahtzugabe deinem Schnittmuster hinzugefügt werden soll
|
||||
complete.t: Details
|
||||
complete.d: Legt fest, wie detailliert das Schnittmuster dargestellt wird; entweder ein vollständiges Schnittmuster mit allen Details oder eine einfache Kontur der Schnittmusterteile
|
||||
expand.t: Expand
|
||||
expand.d: Controls efforts to save paper. Disable this to expand all pattern parts at the cost of using more space.
|
||||
only.t: Enthaltene Teile
|
||||
only.d: Hier kannst du genau festlegen, welche Teile des Musters in deinem Muster enthalten sein sollen.
|
||||
locale.t: Sprache
|
||||
locale.d: Legt die Sprache fest, die für dein Muster verwendet wird. Dies hat keinen Einfluss auf die Sprache der Website, sondern nur auf dieses spezielle Muster.
|
||||
units.t: Einheiten
|
||||
units.d: Diese Einstellung bestimmt, wie die Einheiten auf deinem Muster angezeigt werden
|
||||
margin.t: Randabstand
|
||||
margin.d: Legt den freien Rand um die einzelnen Teile des Schnittmusters fest
|
||||
scale.t: Beschriftungsgröße
|
||||
scale.d: "Steuert die allgemeine Linienbreite, Schriftgröße und andere Elementgrößen, \nbeeinflusst jedoch nicht den Maßstab des Schnittmusters selbst"
|
||||
de.t: Deutsch
|
||||
de.d: Verwende dies, um ein deutsches Muster zu erstellen
|
||||
en.t: Englisch
|
||||
en.d: Benutze dies, um ein englisches Muster zu erzeugen
|
||||
es.t: Spanisch
|
||||
es.d: Verwende dies, um ein spanisches Muster zu erstellen
|
||||
fr.t: Französisch
|
||||
fr.d: Verwende dies, um ein französisches Muster zu erstellen
|
||||
nl.t: Holländisch
|
||||
nl.d: Verwende dies, um ein niederländisches Muster zu erstellen
|
||||
yes: Yes
|
||||
no: No
|
||||
completeYes.t: Ein vollständiges Muster generieren
|
||||
completeYes.d: Dadurch wird ein vollständiges Muster mit allen Notationen, Linien und Markierungen erstellt. Verwende dies, wenn du dir nicht sicher bist, was du wählen sollst.
|
||||
completeNo.t: Erstelle einen Musterumriss
|
||||
completeNo.d: Erstelle nur die Umrisse der Musterteile. Verwende dies, wenn du einen Lasercutter verwenden möchtest oder andere spezielle Anforderungen hast.
|
||||
expandYes.t: Expand all pattern parts
|
||||
expandYes.d: This will generate a pattern where all pattern parts are drawn to their full size, even if they are simple rectangles.
|
||||
expandNo.t: Keep patterns parts compact where possible
|
||||
expandNo.d: This will draw a more dense representation of the pattern which includes all info without using up too much space & paper.
|
||||
paperlessNo.t: Erzeuge ein regelmäßiges Muster
|
||||
paperlessNo.d: Dadurch wird ein regelmäßiges Muster erzeugt, das du dann ausdrucken kannst.
|
||||
paperlessYes.t: Erstelle ein papierloses Muster
|
||||
paperlessYes.d: So entsteht ein Muster mit Maßen und einem Raster, das du auf Stoff oder ein anderes Medium übertragen kannst, ohne dass du das Muster ausdrucken musst.
|
||||
metric: Metrisch
|
||||
imperial: Kaiserlich
|
||||
metric.t: Metrische Einheiten verwenden
|
||||
metric.d: Verwende dies, wenn du das metrische System verwendest und mit Zentimetern und Millimetern vertraut bist. Das ist für die meisten Menschen auf der ganzen Welt die beste Wahl.
|
||||
imperial.t: Imperiale Einheiten verwenden
|
||||
imperial.d: Verwende dies, wenn dir Zoll und Bruchteile von Zoll vertrauter sind als Zentimeter. Dies ist oft die bevorzugte Wahl für Menschen in Großbritannien und den USA.
|
||||
saNo.t: Nahtzugabe nicht berücksichtigen
|
||||
saNo.d: Dadurch wird ein Muster erstellt, das keine Nahtzugabe enthält. Die Größe der Nahtzugabe spielt keine Rolle, da keine Nahtzugabe enthalten ist.
|
||||
saYes.t: Nahtzugabe hinzufügen
|
||||
saYes.d: Dadurch wird ein Muster erstellt, das die Nahtzugabe enthält. Die Größe der Nahtzugabe wird individuell festgelegt.
|
||||
clearSettingsNotMeasurements: Clear settings, but keep measurements
|
||||
clearSettingsAndMeasurements: Clear settings & Clear measurements
|
|
@ -0,0 +1,59 @@
|
|||
coreSettings.t: Core Settings
|
||||
coreSettings.d: These settings are not specific to the design, but instead allow you to customize various parameters of the FreeSewing core library, which generates the design for you.
|
||||
designOptions.t: These options are specific to this design. You can use them to customize your pattern in a variety of ways.
|
||||
paperless.t: Paperless
|
||||
paperless.d: Trees are awesome, and taping together sewing patterns is not much fun. Try our paperless mode to avoid the need to print out your pattern altogether.
|
||||
samm.t: Seam Allowance Size
|
||||
samm.d: Controls the amount of seam allowance used in your pattern
|
||||
sabool.t: Include Seam Allowance
|
||||
sabool.d: Controls whether or not to include seam allowance in your pattern
|
||||
complete.t: Details
|
||||
complete.d: Controls how detailed the pattern is; Either a complete pattern with all details, or a basic outline of the pattern parts
|
||||
expand.t: Expand
|
||||
expand.d: Controls efforts to save paper. Disable this to expand all pattern parts at the cost of using more space.
|
||||
only.t: Included Parts
|
||||
only.d: Use this to control exactly which pattern parts will be included in your pattern
|
||||
locale.t: Language
|
||||
locale.d: Determines the language used on your pattern. This will not influence the language of the website, only of this specific pattern.
|
||||
units.t: Units
|
||||
units.d: This setting determines how unit are displayed on your pattern
|
||||
margin.t: Margin
|
||||
margin.d: Controls the margin around pattern parts
|
||||
scale.t: Scale
|
||||
scale.d: Controls the overall line width, font size, and other elements that do not scale with the pattern's measurements
|
||||
de.t: German
|
||||
de.d: Use this to generate a German pattern
|
||||
en.t: English
|
||||
en.d: Use this to generate an English pattern
|
||||
es.t: Spanish
|
||||
es.d: Use this to generate a Spanish pattern
|
||||
fr.t: French
|
||||
fr.d: Use this to generate a French pattern
|
||||
nl.t: Dutch
|
||||
nl.d: Use this to generate a Dutch pattern
|
||||
yes: Yes
|
||||
no: No
|
||||
completeYes.t: Generate a complete pattern
|
||||
completeYes.d: This will generate a complete pattern with all notations, lines, markings. Use this if you are not certain what to choose.
|
||||
completeNo.t: Generate a pattern outline
|
||||
completeNo.d: Only generate the outline of the pattern parts. Use this if you are looking to use a lasercutter or have other specific needs.
|
||||
expandYes.t: Expand all pattern parts
|
||||
expandYes.d: This will generate a pattern where all pattern parts are drawn to their full size, even if they are simple rectangles.
|
||||
expandNo.t: Keep patterns parts compact where possible
|
||||
expandNo.d: This will draw a more dense representation of the pattern which includes all info without using up too much space & paper.
|
||||
paperlessNo.t: Generate a regular pattern
|
||||
paperlessNo.d: This will generate a regular pattern, which you can then print out.
|
||||
paperlessYes.t: Generate a paperless pattern
|
||||
paperlessYes.d: This generates a pattern with dimensions and a grid, which allows you to transfer it on fabric or another medium without the need to print out the pattern.
|
||||
metric: Metric
|
||||
imperial: Imperial
|
||||
metric.t: Use metric units
|
||||
metric.d: Use this if you use the metric system, and centimeters and millimeters are something you are familiar with. This is the best choice for most people around the world.
|
||||
imperial.t: Use imperial units
|
||||
imperial.d: Use this if inches and fractions or inches are more familiar to you then centimeters. This is often the preferred choice for people based in the UK & US.
|
||||
saNo.t: Do not include seam allowance
|
||||
saNo.d: This generates a pattern which does not include any seam allowance. The size of the seam allowance does not matter as no seam allowancce will be included.
|
||||
saYes.t: Include seam allowance
|
||||
saYes.d: This generates a pattern that will include seam allowance. The size of the seam allowance is set individually.
|
||||
clearSettingsNotMeasurements: Clear settings, but keep measurements
|
||||
clearSettingsAndMeasurements: Clear settings & Clear measurements
|
|
@ -0,0 +1,59 @@
|
|||
coreSettings.t: Configuración del núcleo
|
||||
coreSettings.d: Estos ajustes no son específicos del diseño, sino que te permiten personalizar varios parámetros de la biblioteca central de FreeSewing, que genera el diseño por ti.
|
||||
designOptions.t: Estas opciones son específicas de este diseño. Puedes utilizarlas para personalizar tu patrón de diversas formas.
|
||||
paperless.t: Sin papel
|
||||
paperless.d: Los árboles son increíbles, y pegar patrones de costura no es muy divertido. Prueba nuestro modo sin papel para evitar por completo la necesidad de imprimir tu patrón.
|
||||
samm.t: Tamaño del margen de costura
|
||||
samm.d: Controla la cantidad de margen de costura utilizado en tu patrón
|
||||
sabool.t: Incluir margen de costura
|
||||
sabool.d: Controla si incluir o no margen de costura en tu patrón
|
||||
complete.t: Detalles
|
||||
complete.d: Controla qué tan detallado es el patrón. Ya sea un patrón completo con todos los detalles, o un esquema básico de las partes del patrón
|
||||
expand.t: Expand
|
||||
expand.d: Controls efforts to save paper. Disable this to expand all pattern parts at the cost of using more space.
|
||||
only.t: Piezas incluidas
|
||||
only.d: Utilízalo para controlar exactamente qué partes del patrón se incluirán en tu patrón
|
||||
locale.t: Idioma
|
||||
locale.d: Determina el idioma utilizado en tu patrón. Esto no influirá en el idioma del sitio web, sólo de este patrón específico.
|
||||
units.t: Unidades
|
||||
units.d: Este ajuste determina cómo se muestran las unidades en tu patrón
|
||||
margin.t: Margen
|
||||
margin.d: Controla el margen alrededor de las partes del patrón
|
||||
scale.t: Escala
|
||||
scale.d: Controla el ancho de línea general, el tamaño de fuente y otros elementos que no cambian de escala junto a las medidas del patrón
|
||||
de.t: Alemán
|
||||
de.d: Utilízalo para generar un patrón alemán
|
||||
en.t: Inglés
|
||||
en.d: Utilízalo para generar un patrón inglés
|
||||
es.t: Español
|
||||
es.d: Utilízalo para generar un patrón español
|
||||
fr.t: Francés
|
||||
fr.d: Utilízalo para generar un patrón francés
|
||||
nl.t: Holandés
|
||||
nl.d: Utilízalo para generar un patrón holandés
|
||||
yes: Yes
|
||||
no: No
|
||||
completeYes.t: Generar un patrón completo
|
||||
completeYes.d: Esto generará un patrón completo con todas las anotaciones, líneas y marcas. Utilízalo si no estás seguro de qué elegir.
|
||||
completeNo.t: Generar un esquema de patrón
|
||||
completeNo.d: Genera sólo el contorno de las piezas del patrón. Utilízalo si quieres utilizar una cortadora láser o tienes otras necesidades específicas.
|
||||
expandYes.t: Expand all pattern parts
|
||||
expandYes.d: This will generate a pattern where all pattern parts are drawn to their full size, even if they are simple rectangles.
|
||||
expandNo.t: Keep patterns parts compact where possible
|
||||
expandNo.d: This will draw a more dense representation of the pattern which includes all info without using up too much space & paper.
|
||||
paperlessNo.t: Generar un patrón regular
|
||||
paperlessNo.d: Esto generará un patrón regular, que luego podrás imprimir.
|
||||
paperlessYes.t: Generar un patrón sin papel
|
||||
paperlessYes.d: Esto genera un patrón con dimensiones y una cuadrícula, que te permite transferirlo sobre tela u otro soporte sin necesidad de imprimir el patrón.
|
||||
metric: Métrica
|
||||
imperial: Imperial
|
||||
metric.t: Utiliza unidades métricas
|
||||
metric.d: Utilízalo si usas el sistema métrico decimal, y los centímetros y milímetros son algo con lo que estás familiarizado. Esta es la mejor opción para la mayoría de la gente de todo el mundo.
|
||||
imperial.t: Utiliza unidades imperiales
|
||||
imperial.d: Utilízalo si las pulgadas y fracciones o centímetros te resultan más familiares que los centímetros. Suele ser la opción preferida por las personas que viven en el Reino Unido y EE.UU.
|
||||
saNo.t: No incluyas el margen de costura
|
||||
saNo.d: Esto genera un patrón que no incluye ningún margen de costura. El tamaño del margen de costura no importa, ya que no se incluirá ningún margen de costura.
|
||||
saYes.t: Incluir margen de costura
|
||||
saYes.d: Esto genera un patrón que incluirá el margen de costura. El tamaño del margen de costura se ajusta individualmente.
|
||||
clearSettingsNotMeasurements: Clear settings, but keep measurements
|
||||
clearSettingsAndMeasurements: Clear settings & Clear measurements
|
|
@ -0,0 +1,59 @@
|
|||
coreSettings.t: Paramètres de base
|
||||
coreSettings.d: Ces réglages ne sont pas spécifiques au dessin, mais te permettent plutôt de personnaliser divers paramètres de la bibliothèque de base de FreeSewing, qui génère le dessin pour toi.
|
||||
designOptions.t: Ces options sont spécifiques à ce modèle. Tu peux les utiliser pour personnaliser ton motif de différentes manières.
|
||||
paperless.t: Sans papier
|
||||
paperless.d: Les arbres, c'est génial, et assembler des patrons de couture avec du ruban adhésif, ce n'est pas très amusant. Essaie notre mode sans papier pour éviter d'avoir à imprimer complètement ton patron.
|
||||
samm.t: Taille de la marge de couture
|
||||
samm.d: Contrôle la quantité de marge de couture utilisée dans ton patron.
|
||||
sabool.t: Inclure la marge de couture
|
||||
sabool.d: Contrôle l'inclusion ou non de la marge de couture dans le patron
|
||||
complete.t: Détails
|
||||
complete.d: Contrôle à quel point votre patron est détaillé ; soit un patron complet avec tous les détails, ou simplement les contours des parties du patron
|
||||
expand.t: Expand
|
||||
expand.d: Controls efforts to save paper. Disable this to expand all pattern parts at the cost of using more space.
|
||||
only.t: Pièces incluses
|
||||
only.d: Utilise ceci pour contrôler exactement les parties du modèle qui seront incluses dans ton modèle
|
||||
locale.t: Langue
|
||||
locale.d: Détermine la langue utilisée sur ton motif. Cela n'influencera pas la langue du site Internet, mais seulement celle de ce patron spécifique.
|
||||
units.t: Unités
|
||||
units.d: Ce réglage détermine la façon dont les unités sont affichées sur ton modèle.
|
||||
margin.t: Marges
|
||||
margin.d: Contrôle la marge autour des pièces du patron
|
||||
scale.t: Mise à l'échelle
|
||||
scale.d: Contrôle la largeur de la ligne de contour, la taille de police et les autres éléments dont la valeur est indépendante des mesures du patron
|
||||
de.t: Allemand
|
||||
de.d: Utilise ceci pour générer un modèle allemand
|
||||
en.t: Anglais
|
||||
en.d: Utilise ceci pour générer un modèle anglais
|
||||
es.t: Espagnol
|
||||
es.d: Utilise ceci pour générer un modèle espagnol
|
||||
fr.t: Français
|
||||
fr.d: Utilise ceci pour générer un modèle français
|
||||
nl.t: Néerlandais
|
||||
nl.d: Utilise ceci pour générer un modèle hollandais
|
||||
yes: Yes
|
||||
no: No
|
||||
completeYes.t: Génère un modèle complet
|
||||
completeYes.d: Cela générera un modèle complet avec toutes les notations, les lignes et les marques. Utilise cette fonction si tu n'es pas certain de ce que tu dois choisir.
|
||||
completeNo.t: Génère une ébauche de modèle
|
||||
completeNo.d: Ne génère que le contour des pièces du modèle. Utilise cette option si tu souhaites utiliser une découpeuse laser ou si tu as d'autres besoins spécifiques.
|
||||
expandYes.t: Expand all pattern parts
|
||||
expandYes.d: This will generate a pattern where all pattern parts are drawn to their full size, even if they are simple rectangles.
|
||||
expandNo.t: Keep patterns parts compact where possible
|
||||
expandNo.d: This will draw a more dense representation of the pattern which includes all info without using up too much space & paper.
|
||||
paperlessNo.t: Génère un motif régulier
|
||||
paperlessNo.d: Cela générera un motif régulier, que tu pourras ensuite imprimer.
|
||||
paperlessYes.t: Génère un modèle sans papier
|
||||
paperlessYes.d: Cela génère un patron avec des dimensions et une grille, ce qui te permet de le transférer sur du tissu ou un autre support sans avoir besoin d'imprimer le patron.
|
||||
metric: Métrique
|
||||
imperial: Impérial
|
||||
metric.t: Utilise les unités métriques
|
||||
metric.d: Utilise ceci si tu utilises le système métrique et si les centimètres et les millimètres te sont familiers. C'est le meilleur choix pour la plupart des gens dans le monde.
|
||||
imperial.t: Utilise les unités impériales
|
||||
imperial.d: Utilise ceci si les pouces et les fractions de pouces te sont plus familiers que les centimètres. C'est souvent le choix préféré des personnes basées au Royaume-Uni et aux États-Unis.
|
||||
saNo.t: Ne pas inclure la marge de couture
|
||||
saNo.d: Cela génère un patron qui ne comprend pas de marge de couture. La taille de la marge de couture n'a pas d'importance car aucune marge de couture ne sera incluse.
|
||||
saYes.t: Inclure la marge de couture
|
||||
saYes.d: Cela génère un modèle qui comprendra une marge de couture. La taille de la marge de couture est définie individuellement.
|
||||
clearSettingsNotMeasurements: Clear settings, but keep measurements
|
||||
clearSettingsAndMeasurements: Clear settings & Clear measurements
|
109
sites/shared/components/workbench/menus/core-settings/index.mjs
Normal file
109
sites/shared/components/workbench/menus/core-settings/index.mjs
Normal file
|
@ -0,0 +1,109 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
//Dependencies
|
||||
import { loadSettingsConfig, defaultSamm } from './config.mjs'
|
||||
// Components
|
||||
import { SettingsIcon, TrashIcon } from 'shared/components/icons.mjs'
|
||||
import { WorkbenchMenu } from '../shared/index.mjs'
|
||||
import { MenuItem } from '../shared/menu-item.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
// input components and event handlers
|
||||
import { inputs, handlers } from './inputs.mjs'
|
||||
// values
|
||||
import { values } from './values.mjs'
|
||||
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const ns = ['core-settings', 'modal']
|
||||
|
||||
/** A wrapper for {@see MenuItem} to handle core settings-specific business */
|
||||
const CoreSetting = ({ name, config, control, updateFunc, current, passProps, ...rest }) => {
|
||||
// is toggling allowed?
|
||||
const allowToggle = control > 3 && config.list?.length === 2
|
||||
|
||||
const handlerArgs = {
|
||||
updateFunc,
|
||||
current,
|
||||
config,
|
||||
...passProps,
|
||||
}
|
||||
// get the appropriate event handler if there is one
|
||||
const handler = handlers[name] ? handlers[name](handlerArgs) : updateFunc
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
{...{
|
||||
name,
|
||||
config,
|
||||
control,
|
||||
current,
|
||||
passProps,
|
||||
...rest,
|
||||
allowToggle,
|
||||
updateFunc: handler,
|
||||
}}
|
||||
docs={
|
||||
<DynamicMdx
|
||||
language={rest.language}
|
||||
slug={`docs/site/draft/core-settings/${name.toLowerCase()}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ClearAllButton = ({ setSettings, compact = false }) => {
|
||||
const { t } = useTranslation('core-settings')
|
||||
return (
|
||||
<div className={`${compact ? '' : 'text-center mt-8'}`}>
|
||||
<button
|
||||
className={`justify-self-center btn btn-error btn-outline ${compact ? 'btn-sm' : ''}`}
|
||||
onClick={() => setSettings({})}
|
||||
>
|
||||
<TrashIcon />
|
||||
{t('clearSettings')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The core settings menu
|
||||
* @param {Object} options.update settings and ui update functions
|
||||
* @param {Object} options.settings core settings
|
||||
* @param {Object} options.patternConfig the configuration from the pattern
|
||||
* @param {String} options.language the menu language
|
||||
* @param {Object} options.account the user account data
|
||||
*/
|
||||
export const CoreSettings = ({ update, settings, patternConfig, language, account, design }) => {
|
||||
const settingsConfig = loadSettingsConfig({
|
||||
language,
|
||||
units: settings.units,
|
||||
sabool: settings.sabool,
|
||||
parts: patternConfig.draftOrder,
|
||||
})
|
||||
|
||||
const passProps = {
|
||||
samm: typeof settings.samm === 'undefined' ? defaultSamm(settings.units) : settings.samm,
|
||||
units: settings.units,
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkbenchMenu
|
||||
{...{
|
||||
config: settingsConfig,
|
||||
control: account.control,
|
||||
currentValues: settings,
|
||||
Icon: SettingsIcon,
|
||||
inputs,
|
||||
language,
|
||||
name: 'coreSettings',
|
||||
ns,
|
||||
passProps,
|
||||
design,
|
||||
updateFunc: update.settings,
|
||||
values,
|
||||
Item: CoreSetting,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { capitalize } from 'shared/utils.mjs'
|
||||
import { ListInput, SliderInput, BoolInput, MmInput } from '../shared/inputs.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { collection } from 'site/hooks/use-design.mjs'
|
||||
|
||||
/** an input for the 'only' setting. toggles individual parts*/
|
||||
const OnlySettingInput = (props) => {
|
||||
const { t } = useTranslation(collection)
|
||||
const { config } = props
|
||||
config.sideBySide = true
|
||||
config.titleMethod = (entry, t) => {
|
||||
const chunks = entry.split('.')
|
||||
return <span className="font-medium text-base">{t(`${chunks[0]}:${chunks[1]}`)}</span>
|
||||
}
|
||||
config.valueMethod = (entry) => <span className="text-sm">{capitalize(entry.split('.')[0])}</span>
|
||||
config.dense = true
|
||||
// Sort alphabetically (translated)
|
||||
const order = []
|
||||
for (const part of config.list) {
|
||||
const [ns, name] = part.split('.')
|
||||
order.push(t(`${ns}:${name}`) + `|${part}`)
|
||||
}
|
||||
order.sort()
|
||||
config.list = order.map((entry) => entry.split('|')[1])
|
||||
|
||||
return <ListInput {...props} />
|
||||
}
|
||||
|
||||
export const inputs = {
|
||||
complete: ListInput,
|
||||
expand: ListInput,
|
||||
locale: ListInput,
|
||||
margin: MmInput,
|
||||
only: OnlySettingInput,
|
||||
paperless: BoolInput,
|
||||
sabool: BoolInput,
|
||||
samm: MmInput,
|
||||
scale: SliderInput,
|
||||
units: BoolInput,
|
||||
}
|
||||
|
||||
/** custom event handlers for inputs that need them */
|
||||
export const handlers = {
|
||||
only:
|
||||
({ updateFunc, current }) =>
|
||||
(path, part) => {
|
||||
// if there's no part being set, it's a reset
|
||||
if (part === undefined) return updateFunc(path, part)
|
||||
|
||||
// add or remove the part from the set
|
||||
let newParts = new Set(current || [])
|
||||
if (newParts.has(part)) newParts.delete(part)
|
||||
else newParts.add(part)
|
||||
|
||||
// if the set is now empty, reset
|
||||
if (newParts.size < 1) newParts = undefined
|
||||
// otherwise use the new set
|
||||
else newParts = [...newParts]
|
||||
|
||||
updateFunc(path, newParts)
|
||||
},
|
||||
samm:
|
||||
({ updateFunc, config }) =>
|
||||
(_path, newCurrent) => {
|
||||
// convert to millimeters if there's a value
|
||||
newCurrent = newCurrent === undefined ? config.dflt : newCurrent
|
||||
// update both values to match
|
||||
updateFunc([
|
||||
[['samm'], newCurrent],
|
||||
[['sa'], newCurrent],
|
||||
])
|
||||
},
|
||||
sabool:
|
||||
({ updateFunc, samm }) =>
|
||||
(_path, newCurrent) => {
|
||||
updateFunc([
|
||||
// update sabool to the new current
|
||||
[['sabool'], newCurrent],
|
||||
// set sa based on whether there's a current value or not
|
||||
[['sa'], newCurrent ? samm : undefined],
|
||||
])
|
||||
},
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
coreSettings.t: Kerninstellingen
|
||||
coreSettings.d: Deze instellingen zijn niet specifiek voor het ontwerp, maar stellen je in staat om verschillende parameters aan te passen van de FreeSewing kernbibliotheek, die het ontwerp voor je genereert.
|
||||
designOptions.t: Deze opties zijn specifiek voor dit ontwerp. Je kunt ze gebruiken om je patroon op verschillende manieren aan te passen.
|
||||
paperless.t: Papierloos
|
||||
paperless.d: Bomen zijn geweldig en naaipatronen in elkaar tikken is niet leuk. Probeer onze papierloze modus om te voorkomen dat je je patroon helemaal moet uitprinten.
|
||||
samm.t: Naadtoeslag Maat
|
||||
samm.d: Bepaalt de hoeveelheid naadtoeslag die in je patroon wordt gebruikt
|
||||
sabool.t: Naadtoeslag opnemen
|
||||
sabool.d: Bepaalt of je wel of geen naadtoeslag in je patroon wilt opnemen
|
||||
complete.t: Details
|
||||
complete.d: Bepaalt hoe gedetailleerd het patroon is; Ofwel een patroon met alle details, ofwel een eenvoudiger patroon met slechts de contouren van de verschillende patroondelen
|
||||
expand.t: Expand
|
||||
expand.d: Controls efforts to save paper. Disable this to expand all pattern parts at the cost of using more space.
|
||||
only.t: Meegeleverde onderdelen
|
||||
only.d: Gebruik dit om precies te bepalen welke patroondelen in je patroon worden opgenomen
|
||||
locale.t: Taal
|
||||
locale.d: Bepaalt de taal van je patroon. Dit heeft geen invloed op de taal van de website, alleen van dit specifieke patroon.
|
||||
units.t: Eenheden
|
||||
units.d: Deze instelling bepaalt hoe eenheden worden weergegeven op je patroon
|
||||
margin.t: Marge
|
||||
margin.d: Bepaalt de marge rond patroondelen
|
||||
scale.t: Schaal
|
||||
scale.d: Bepaalt de totale lijnbreedte, lettergrootte en andere elementen die niet schalen met de metingen van het patroon
|
||||
de.t: Duits
|
||||
de.d: Gebruik dit om een Duits patroon te genereren
|
||||
en.t: Engels
|
||||
en.d: Gebruik dit om een Engels patroon te genereren
|
||||
es.t: Spaans
|
||||
es.d: Gebruik dit om een Spaans patroon te genereren
|
||||
fr.t: Frans
|
||||
fr.d: Gebruik dit om een Frans patroon te genereren
|
||||
nl.t: Nederlands
|
||||
nl.d: Gebruik dit om een Nederlands patroon te genereren
|
||||
yes: Yes
|
||||
no: No
|
||||
completeYes.t: Genereer een compleet patroon
|
||||
completeYes.d: Dit genereert een compleet patroon met alle notaties, lijnen en markeringen. Gebruik dit als je niet zeker weet wat je moet kiezen.
|
||||
completeNo.t: Genereer een patroonoverzicht
|
||||
completeNo.d: Genereer alleen de omtrek van de patroondelen. Gebruik dit als je een lasercutter wilt gebruiken of andere specifieke wensen hebt.
|
||||
expandYes.t: Expand all pattern parts
|
||||
expandYes.d: This will generate a pattern where all pattern parts are drawn to their full size, even if they are simple rectangles.
|
||||
expandNo.t: Keep patterns parts compact where possible
|
||||
expandNo.d: This will draw a more dense representation of the pattern which includes all info without using up too much space & paper.
|
||||
paperlessNo.t: Een regelmatig patroon genereren
|
||||
paperlessNo.d: Dit genereert een regelmatig patroon dat je vervolgens kunt afdrukken.
|
||||
paperlessYes.t: Een papierloos patroon genereren
|
||||
paperlessYes.d: Dit genereert een patroon met afmetingen en een raster, waardoor je het kunt overbrengen op stof of een ander medium zonder het patroon te hoeven afdrukken.
|
||||
metric: Metrisch
|
||||
imperial: Keizerlijk
|
||||
metric.t: Gebruik metrische eenheden
|
||||
metric.d: Gebruik deze als je het metrische stelsel gebruikt en centimeters en millimeters iets zijn waar je bekend mee bent. Dit is de beste keuze voor de meeste mensen over de hele wereld.
|
||||
imperial.t: Gebruik imperiale eenheden
|
||||
imperial.d: Gebruik dit als je meer vertrouwd bent met inches en fracties of inches dan met centimeters. Dit is vaak de voorkeursoptie voor mensen in het Verenigd Koninkrijk en de Verenigde Staten.
|
||||
saNo.t: Exclusief naadtoeslag
|
||||
saNo.d: Dit genereert een patroon zonder naadtoeslag. De grootte van de naadtoeslag maakt niet uit, omdat er geen naadtoeslag wordt opgenomen.
|
||||
saYes.t: Inclusief naadtoeslag
|
||||
saYes.d: Dit genereert een patroon met naadtoeslag. De grootte van de naadtoeslag wordt individueel ingesteld.
|
||||
clearSettingsNotMeasurements: Clear settings, but keep measurements
|
||||
clearSettingsAndMeasurements: Clear settings & Clear measurements
|
|
@ -0,0 +1,59 @@
|
|||
coreSettings.t: Основні налаштування
|
||||
coreSettings.d: Ці налаштування не є специфічними для дизайну, а дозволяють вам налаштувати різні параметри основної бібліотеки FreeSewing, яка генерує дизайн для вас.
|
||||
designOptions.t: Ці опції є специфічними для цього дизайну. Ви можете використовувати їх, щоб налаштувати свій шаблон у різний спосіб.
|
||||
paperless.t: Без паперу
|
||||
paperless.d: Дерева - це круто, а склеювати викрійки не дуже весело. Спробуйте наш безпаперовий режим, щоб уникнути необхідності роздруковувати викрійку.
|
||||
samm.t: Розмір припуску на шов
|
||||
samm.d: Керує величиною припусків на шви, що використовуються у вашій деталі
|
||||
sabool.t: Включити припуски на шви
|
||||
sabool.d: Керує наявністю припусків на шви в Вашій викрійці
|
||||
complete.t: Деталі
|
||||
complete.d: 'Керує докладність викрійки: відображувати повноцінну викрійку з усіма подробицями чи лише основний контур елементів викрійки'
|
||||
expand.t: Expand
|
||||
expand.d: Controls efforts to save paper. Disable this to expand all pattern parts at the cost of using more space.
|
||||
only.t: Деталі, що входять до комплекту
|
||||
only.d: Використовуйте цей параметр, щоб точно контролювати, які деталі будуть включені в деталь
|
||||
locale.t: Мова
|
||||
locale.d: Визначає мову, яка використовується у вашому шаблоні. Це не вплине на мову веб-сайту, лише на мову цього конкретного шаблону.
|
||||
units.t: Одиниці
|
||||
units.d: Цей параметр визначає спосіб відображення одиниць виміру на лекалах
|
||||
margin.t: Маржа.
|
||||
margin.d: Контролює відступ навколо елементів викрійки
|
||||
scale.t: Масштаб
|
||||
scale.d: Керує ширину контуру, кегль шрифту та інші елементи, які не масштабуються відповідно до замірів викрійки
|
||||
de.t: Німецька
|
||||
de.d: Скористайтеся цим, щоб згенерувати німецький шаблон
|
||||
en.t: Англійська мова
|
||||
en.d: Використовуйте це, щоб згенерувати англійський шаблон
|
||||
es.t: Іспанська
|
||||
es.d: Скористайтеся цим, щоб створити іспанський шаблон
|
||||
fr.t: Французька
|
||||
fr.d: Використовуйте це, щоб створити французький візерунок
|
||||
nl.t: Голландська
|
||||
nl.d: Використовуйте це, щоб створити голландський візерунок
|
||||
yes: Yes
|
||||
no: No
|
||||
completeYes.t: Створіть повний шаблон
|
||||
completeYes.d: В результаті буде згенеровано повну викрійку з усіма позначеннями, лініями, маркуванням. Використовуйте цю функцію, якщо ви не впевнені, що вибрати.
|
||||
completeNo.t: Створіть контур деталі
|
||||
completeNo.d: Створюйте лише контури деталей лекал. Використовуйте цей параметр, якщо ви збираєтеся використовувати лазерний різак або маєте інші специфічні потреби.
|
||||
expandYes.t: Expand all pattern parts
|
||||
expandYes.d: This will generate a pattern where all pattern parts are drawn to their full size, even if they are simple rectangles.
|
||||
expandNo.t: Keep patterns parts compact where possible
|
||||
expandNo.d: This will draw a more dense representation of the pattern which includes all info without using up too much space & paper.
|
||||
paperlessNo.t: Створіть регулярний шаблон
|
||||
paperlessNo.d: В результаті буде створено звичайний шаблон, який ви можете роздрукувати.
|
||||
paperlessYes.t: Створіть безпаперовий шаблон
|
||||
paperlessYes.d: При цьому генерується викрійка з розмірами і сіткою, що дозволяє перенести її на тканину або інший носій без необхідності роздруковувати викрійку.
|
||||
metric: Метрика
|
||||
imperial: Імперіал
|
||||
metric.t: Використовуйте метричні одиниці
|
||||
metric.d: Використовуйте його, якщо ви користуєтеся метричною системою, а сантиметри і міліметри - це те, що вам знайоме. Це найкращий вибір для більшості людей у всьому світі.
|
||||
imperial.t: Використовуйте імперські одиниці
|
||||
imperial.d: Використовуйте його, якщо дюйми та дроби або дюйми більш звичні для вас, ніж сантиметри. Це часто кращий вибір для людей, які живуть у Великій Британії та США.
|
||||
saNo.t: Не враховувати припуски на шви
|
||||
saNo.d: При цьому створюється деталь без припусків на шви. Розмір припуску на шов не має значення, оскільки він не буде врахований.
|
||||
saYes.t: Враховувати припуски на шви
|
||||
saYes.d: При цьому генерується деталь з припусками на шви. Розмір припуску на шов задається індивідуально.
|
||||
clearSettingsNotMeasurements: Clear settings, but keep measurements
|
||||
clearSettingsAndMeasurements: Clear settings & Clear measurements
|
|
@ -0,0 +1,27 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { ListValue, MmValue, PlainValue } from '../shared/values'
|
||||
|
||||
const ScaleSettingValue = ({ current, config, changed }) => (
|
||||
<PlainValue current={current} dflt={config.dflt} changed={changed} />
|
||||
)
|
||||
|
||||
const OnlySettingValue = ({ current, config }) => (
|
||||
<PlainValue
|
||||
current={current?.length}
|
||||
dflt={config.parts.length}
|
||||
changed={current !== undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
export const values = {
|
||||
complete: ListValue,
|
||||
expand: ListValue,
|
||||
locale: ListValue,
|
||||
margin: MmValue,
|
||||
only: OnlySettingValue,
|
||||
paperless: ListValue,
|
||||
sabool: ListValue,
|
||||
samm: MmValue,
|
||||
scale: ScaleSettingValue,
|
||||
units: ListValue,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
designOptions.t: Gestaltungsmöglichkeiten
|
||||
designOptions.d: Diese Optionen sind spezifisch für dieses Muster. Du kannst sie verwenden, um dein Muster auf verschiedene Weise anzupassen.
|
||||
fit.t: Passform
|
||||
style.t: Stil
|
||||
advanced.t: Fortgeschritten
|
|
@ -0,0 +1,5 @@
|
|||
designOptions.t: Design Options
|
||||
designOptions.d: These options are specific to this design. You can use them to customize your pattern in a variety of ways.
|
||||
fit.t: Fit
|
||||
style.t: Style
|
||||
advanced.t: Advanced
|
|
@ -0,0 +1,5 @@
|
|||
designOptions.t: Opciones de diseño
|
||||
designOptions.d: Estas opciones son específicas de este diseño. Puedes utilizarlas para personalizar tu patrón de diversas formas.
|
||||
fit.t: Ajuste
|
||||
style.t: Estilo
|
||||
advanced.t: Avanzado
|
|
@ -0,0 +1,5 @@
|
|||
designOptions.t: Options de conception
|
||||
designOptions.d: Ces options sont spécifiques à ce modèle. Tu peux les utiliser pour personnaliser ton motif de différentes manières.
|
||||
fit.t: Ajustement
|
||||
style.t: Style
|
||||
advanced.t: Avancé
|
110
sites/shared/components/workbench/menus/design-options/index.mjs
Normal file
110
sites/shared/components/workbench/menus/design-options/index.mjs
Normal file
|
@ -0,0 +1,110 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useCallback, useMemo } from 'react'
|
||||
// Components
|
||||
import { OptionsIcon } from 'shared/components/icons.mjs'
|
||||
import { optionsMenuStructure, optionType } from 'shared/utils.mjs'
|
||||
import { values } from './values.mjs'
|
||||
import { inputs } from './inputs.mjs'
|
||||
import { WorkbenchMenu } from '../shared/index.mjs'
|
||||
import { MenuItem } from '../shared/menu-item.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['design-options']
|
||||
|
||||
// Emojis for option groups :)
|
||||
export const emojis = {
|
||||
advanced: '🤓',
|
||||
fit: '👕',
|
||||
style: '💃🏽',
|
||||
dflt: '🕹️',
|
||||
groupDflt: '📁',
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for {@see MenuItem} to handle design option-specific business
|
||||
* @param {Object} options.config the config for the item
|
||||
* @param {Object} options.settings core settings
|
||||
* @param {Object} options.rest the rest of the props
|
||||
*/
|
||||
const DesignOption = ({ config, settings, control, ...rest }) => {
|
||||
const type = optionType(config)
|
||||
const Input = inputs[type]
|
||||
const Value = values[type]
|
||||
const allowOverride = ['pct', 'count', 'deg'].includes(type)
|
||||
const allowToggle =
|
||||
(control > 3 && type === 'bool') || (type == 'list' && config.list.length === 2)
|
||||
|
||||
// Hide option?
|
||||
if (config?.hide || (typeof config?.hide === 'function' && config.hide(settings))) return null
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
{...{
|
||||
config,
|
||||
control,
|
||||
...rest,
|
||||
Input,
|
||||
Value,
|
||||
allowOverride,
|
||||
allowToggle,
|
||||
}}
|
||||
docs={
|
||||
<DynamicMdx
|
||||
language={rest.language}
|
||||
slug={`docs/designs/${rest.design}/options/${rest.name.toLowerCase()}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The design options menu
|
||||
* @param {String} options.design the name of the design
|
||||
* @param {Object} options.patternConfig the configuration from the pattern
|
||||
* @param {Object} options.settings core settings
|
||||
* @param {Object} options.update settings and ui update functions
|
||||
* @param {String} options.language the menu language
|
||||
* @param {Object} options.account the user account data
|
||||
*/
|
||||
export const DesignOptions = ({
|
||||
design,
|
||||
patternConfig,
|
||||
settings,
|
||||
update,
|
||||
language,
|
||||
account,
|
||||
isFirst = true,
|
||||
}) => {
|
||||
const menuNs = [design, ...ns]
|
||||
const optionsMenu = useMemo(
|
||||
() => optionsMenuStructure(patternConfig.options, settings),
|
||||
[patternConfig, settings]
|
||||
)
|
||||
const updateFunc = useCallback(
|
||||
(name, value) => update.settings(['options', ...name], value),
|
||||
[update]
|
||||
)
|
||||
|
||||
return (
|
||||
<WorkbenchMenu
|
||||
{...{
|
||||
config: optionsMenu,
|
||||
control: account.control,
|
||||
currentValues: settings.options,
|
||||
emojis,
|
||||
Icon: OptionsIcon,
|
||||
Item: DesignOption,
|
||||
isFirst,
|
||||
name: 'design-options:designOptions',
|
||||
language,
|
||||
ns: menuNs,
|
||||
passProps: { settings, patternConfig },
|
||||
updateFunc,
|
||||
values,
|
||||
isDesignOptionsGroup: true,
|
||||
design,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { formatMm } from 'shared/utils.mjs'
|
||||
import { mergeOptions } from '@freesewing/core'
|
||||
import {
|
||||
BoolInput,
|
||||
ConstantInput,
|
||||
SliderInput,
|
||||
DegInput,
|
||||
ListInput,
|
||||
PctInput,
|
||||
} from '../shared/inputs.mjs'
|
||||
|
||||
const PctOptionInput = (props) => {
|
||||
const { config, settings, changed } = props
|
||||
const currentOrDefault = changed ? props.current : config.dflt / 100
|
||||
|
||||
return (
|
||||
<PctInput {...props}>
|
||||
<div className="flex flex-row justify-around">
|
||||
<span className={changed ? 'text-accent' : 'text-secondary'}>
|
||||
{config.toAbs
|
||||
? formatMm(
|
||||
config.toAbs(
|
||||
currentOrDefault,
|
||||
settings,
|
||||
mergeOptions(settings, props.patternConfig.options)
|
||||
)
|
||||
)
|
||||
: ' '}
|
||||
</span>
|
||||
</div>
|
||||
</PctInput>
|
||||
)
|
||||
}
|
||||
|
||||
// Facilitate lookup of the input component
|
||||
export const inputs = {
|
||||
bool: BoolInput,
|
||||
constant: ConstantInput,
|
||||
count: (props) => <SliderInput {...props} config={{ ...props.config, step: 1 }} />,
|
||||
deg: DegInput,
|
||||
list: (props) => <ListInput {...props} isDesignOption />,
|
||||
mm: () => <span>FIXME: Mm options are deprecated. Please report this </span>,
|
||||
pct: PctOptionInput,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
designOptions.t: Ontwerpopties
|
||||
designOptions.d: Deze opties zijn specifiek voor dit ontwerp. Je kunt ze gebruiken om je patroon op verschillende manieren aan te passen.
|
||||
fit.t: Pasvorm
|
||||
style.t: Stijl
|
||||
advanced.t: Geavanceerd
|
|
@ -0,0 +1,5 @@
|
|||
designOptions.t: Варіанти дизайну
|
||||
designOptions.d: Ці опції є специфічними для цього дизайну. Ви можете використовувати їх, щоб налаштувати свій шаблон у різний спосіб.
|
||||
fit.t: За розміром
|
||||
style.t: Стиль
|
||||
advanced.t: Додатково
|
|
@ -0,0 +1,56 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { formatMm, formatPercentage } from 'shared/utils.mjs'
|
||||
import { ListValue, HighlightedValue, PlainValue, BoolValue } from '../shared/values'
|
||||
import { mergeOptions } from '@freesewing/core'
|
||||
|
||||
/** Displays the current percentage value, and the absolute value if configured */
|
||||
export const PctOptionValue = ({ config, current, settings, changed, patternConfig }) => {
|
||||
const val = changed ? current : config.pct / 100
|
||||
|
||||
return (
|
||||
<HighlightedValue changed={changed}>
|
||||
{formatPercentage(val)}
|
||||
{config.toAbs && settings?.measurements
|
||||
? ` | ${formatMm(
|
||||
config.toAbs(val, settings, mergeOptions(settings, patternConfig.options))
|
||||
)}`
|
||||
: null}
|
||||
</HighlightedValue>
|
||||
)
|
||||
}
|
||||
|
||||
/** Displays a count value*/
|
||||
export const CountOptionValue = ({ config, current, changed }) => (
|
||||
<PlainValue {...{ current, changed, dflt: config.count }} />
|
||||
)
|
||||
|
||||
/** Displays a list option value */
|
||||
export const ListOptionValue = (props) => (
|
||||
<ListValue {...props} t={(input) => props.t(`${props.design}:${props.config.name}.${input}.t`)} />
|
||||
)
|
||||
|
||||
/** Displays a degree value */
|
||||
export const DegOptionValue = ({ config, current, changed }) => (
|
||||
<HighlightedValue changed={changed}> {changed ? current : config.deg}°</HighlightedValue>
|
||||
)
|
||||
|
||||
/** Displays the MmOptions are not supported */
|
||||
export const MmOptionValue = () => (
|
||||
<span className="text-error">FIXME: No Mm Options are not supported</span>
|
||||
)
|
||||
|
||||
/** Displays that constant values are not implemented in the front end */
|
||||
export const ConstantOptionValue = () => (
|
||||
<span className="text-error">FIXME: No ConstantOptionvalue implemented</span>
|
||||
)
|
||||
|
||||
// Facilitate lookup of the value component
|
||||
export const values = {
|
||||
bool: BoolValue,
|
||||
constant: ConstantOptionValue,
|
||||
count: CountOptionValue,
|
||||
deg: DegOptionValue,
|
||||
list: ListOptionValue,
|
||||
mm: MmOptionValue,
|
||||
pct: PctOptionValue,
|
||||
}
|
95
sites/shared/components/workbench/menus/mobile-menubar.mjs
Normal file
95
sites/shared/components/workbench/menus/mobile-menubar.mjs
Normal file
|
@ -0,0 +1,95 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useContext, useState, useEffect } from 'react'
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
import { CloseIcon } from 'shared/components/icons.mjs'
|
||||
import { MobileMenubarContext } from 'shared/context/mobile-menubar-context.mjs'
|
||||
import { MenuAltIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
/**
|
||||
* A component to display menu buttons and actions in mobile.
|
||||
* Draws its contents from items added to the {@link MobileMenubarContext}
|
||||
* @returns
|
||||
*/
|
||||
export const MobileMenubar = () => {
|
||||
const { setModal, clearModal, modalContent } = useContext(ModalContext)
|
||||
const { menus, actions } = useContext(MobileMenubarContext)
|
||||
const [selectedModal, setSelectedModal] = useState(false)
|
||||
|
||||
// get the content of the selected modal because this is what will be changing if there are updates
|
||||
const selectedMenu = menus[selectedModal]
|
||||
|
||||
// when the content changes, or the selection changes
|
||||
useEffect(() => {
|
||||
// there's no selected modal, we're in the clear
|
||||
if (!selectedModal) return
|
||||
|
||||
// generate a new modal with the content
|
||||
const Modal = () => {
|
||||
const closeModal = () => {
|
||||
setSelectedModal(false)
|
||||
clearModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalWrapper
|
||||
slideFrom="right"
|
||||
keepOpenOnClick={selectedMenu.keepOpenOnClick}
|
||||
keepOpenOnSwipe
|
||||
fullWidth
|
||||
>
|
||||
<div className="mb-16">{selectedMenu.menuContent}</div>
|
||||
<button
|
||||
className="btn btn-accent btn-circle fixed bottom-4 right-4 z-20"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
// set it
|
||||
setModal(Modal)
|
||||
}, [selectedMenu, selectedModal, clearModal, setModal])
|
||||
|
||||
// clear the selection if the modal was cleared externally
|
||||
useEffect(() => {
|
||||
if (modalContent === null) {
|
||||
setSelectedModal(false)
|
||||
}
|
||||
}, [modalContent, setSelectedModal])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
lg:hidden
|
||||
sticky bottom-0 w-20 -ml-20 self-end
|
||||
duration-300 transition-all
|
||||
flex flex-col-reverse gap-2 mb-2
|
||||
z-20
|
||||
mobile-menubar
|
||||
`}
|
||||
>
|
||||
{Object.keys(menus)
|
||||
.sort((a, b) => menus[a].order - menus[b].order)
|
||||
.map((m) => {
|
||||
const Icon = m === 'nav' ? MenuAltIcon : menus[m].Icon
|
||||
return (
|
||||
<button
|
||||
key={m}
|
||||
className={`btn ${m === 'nav' ? 'btn-neutral' : 'btn-primary'} btn-circle mx-4`}
|
||||
onClick={() => setSelectedModal(m)}
|
||||
>
|
||||
<Icon />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{Object.keys(actions)
|
||||
.sort((a, b) => actions[a].order - actions[b].order)
|
||||
.map((a) => (
|
||||
<div key={a}>{actions[a].actionContent}</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
66
sites/shared/components/workbench/menus/shared/index.mjs
Normal file
66
sites/shared/components/workbench/menus/shared/index.mjs
Normal file
|
@ -0,0 +1,66 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { MenuItemGroup } from './menu-item.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
/**
|
||||
* A component for a collapsible sidebar menu in workbench views
|
||||
* @param {Function} options.updateFunc a function the menu's inputs will use to update their values
|
||||
* @param {String[]} options.ns namespaces used by this menu
|
||||
* @param {React.Component} options.Icon the menu's icon
|
||||
* @param {String} options.name the translation key for the menu's title
|
||||
* @param {Object} options.config the structure of the menu's options
|
||||
* @param {Number} options.control the user's control level setting
|
||||
* @param {Object} options.inputs a map of input components to use, keyed by option name
|
||||
* @param {Object} options.values a map of value components to use, keyed by option name
|
||||
* @param {Object} options.currentValues a map of the values of the menu's options
|
||||
* @param {Object} options.passProps any additional properties to pass the the inputs
|
||||
* @param {string} language the language to use for the menu
|
||||
* @param {Object} emojis a map of the emojis to use, keyed by option name
|
||||
* @param {React.component} Item the component to use for menu items
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
export const WorkbenchMenu = ({
|
||||
updateFunc,
|
||||
ns,
|
||||
Icon = () => null,
|
||||
config,
|
||||
control,
|
||||
inputs,
|
||||
values,
|
||||
currentValues,
|
||||
passProps = {},
|
||||
language,
|
||||
emojis,
|
||||
Item,
|
||||
children,
|
||||
isDesignOptionsGroup,
|
||||
design,
|
||||
}) => {
|
||||
// get translation for the menu
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return children ? (
|
||||
children
|
||||
) : (
|
||||
<MenuItemGroup
|
||||
{...{
|
||||
collapsible: false,
|
||||
topLevel: true,
|
||||
control,
|
||||
currentValues,
|
||||
structure: config,
|
||||
Item,
|
||||
Icon,
|
||||
values,
|
||||
inputs,
|
||||
passProps,
|
||||
updateFunc,
|
||||
emojis,
|
||||
t,
|
||||
language,
|
||||
isDesignOptionsGroup,
|
||||
design,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
519
sites/shared/components/workbench/menus/shared/inputs.mjs
Normal file
519
sites/shared/components/workbench/menus/shared/inputs.mjs
Normal file
|
@ -0,0 +1,519 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useCallback, useMemo, useState, useEffect, useRef } from 'react'
|
||||
import {
|
||||
round,
|
||||
measurementAsMm,
|
||||
measurementAsUnits,
|
||||
formatFraction128,
|
||||
fractionToDecimal,
|
||||
} from 'shared/utils.mjs'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import { ButtonFrame } from 'shared/components/inputs.mjs'
|
||||
|
||||
/*******************************************************************************************
|
||||
* This file contains the base components to be used by inputs in menus in the workbench
|
||||
* For the purposes of our menus, we have two main types:
|
||||
* Sliders for changing numbers
|
||||
* Lists for changing everything else
|
||||
*
|
||||
* Inputs that deal with more specific use cases should wrap one of the above base inputs
|
||||
*******************************************************************************************/
|
||||
|
||||
/** Regex to validate that an input is a number */
|
||||
const numberInputMatchers = {
|
||||
0: /^-?[0-9]*[.,eE]?[0-9]+$/, // match a single decimal separator
|
||||
1: /^-?[0-9]*(\s?[0-9]+\/|[.,eE])?[0-9]+$/, // match a single decimal separator or fraction
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and parse a value that should be a number
|
||||
* @param {any} val the value to validate
|
||||
* @param {Boolean} allowFractions should fractions be considered valid input?
|
||||
* @param {Number} min the minimum allowable value
|
||||
* @param {Number} max the maximum allowable value
|
||||
* @return {null|false|Number} null if the value is empty,
|
||||
* false if the value is invalid,
|
||||
* or the value parsed to a number if it is valid
|
||||
*/
|
||||
const validateVal = (val, allowFractions = true, min = -Infinity, max = Infinity) => {
|
||||
// if it's empty, we're neutral
|
||||
if (typeof val === 'undefined' || val === '') return null
|
||||
|
||||
// make sure it's a string
|
||||
val = ('' + val).trim()
|
||||
|
||||
// get the appropriate match pattern and check for a match
|
||||
const matchPattern = numberInputMatchers[Number(allowFractions)]
|
||||
if (!val.match(matchPattern)) return false
|
||||
|
||||
// replace comma with period
|
||||
const parsedVal = val.replace(',', '.')
|
||||
// if fractions are allowed, parse for fractions, otherwise use the number as a value
|
||||
const useVal = allowFractions ? fractionToDecimal(parsedVal) : parsedVal
|
||||
|
||||
// check that it's a number and it's in the range
|
||||
if (isNaN(useVal) || useVal > max || useVal < min) return false
|
||||
|
||||
// all checks passed. return the parsed value
|
||||
return useVal
|
||||
}
|
||||
|
||||
/**
|
||||
* A number input that accepts comma or period decimal separators.
|
||||
* Because our use case is almost never going to include thousands, we're using a very simple way of accepting commas:
|
||||
* The validator checks for the presence of a single comma or period followed by numbers
|
||||
* The parser replaces a single comma with a period
|
||||
*
|
||||
* optionally accepts fractions
|
||||
* @param {Number} options.val the value of the input
|
||||
* @param {Function} options.onUpdate a function to handle when the value is updated to a valid value
|
||||
* @param {Boolean} options.fractions should the input allow fractional input
|
||||
*/
|
||||
export const NumberInput = ({
|
||||
value,
|
||||
onUpdate,
|
||||
onMount,
|
||||
className,
|
||||
fractions = true,
|
||||
min = -Infinity,
|
||||
max = Infinity,
|
||||
}) => {
|
||||
const valid = useRef(validateVal(value, fractions, min, max))
|
||||
|
||||
// this is the change handler that will be debounced by the debounce handler
|
||||
// we check validity inside this debounced function because
|
||||
// we need to call the debounce handler on change regardless of validity
|
||||
// if we don't, the displayed value won't update
|
||||
const handleChange = useCallback(
|
||||
(newVal) => {
|
||||
// only actually update if the value is valid
|
||||
if (typeof onUpdate === 'function') {
|
||||
onUpdate(valid.current, newVal)
|
||||
}
|
||||
},
|
||||
[onUpdate, valid]
|
||||
)
|
||||
|
||||
// get a debounce handler
|
||||
const { debouncedHandleChange, displayVal } = useDebouncedHandlers({ handleChange, val: value })
|
||||
|
||||
// onChange
|
||||
const onChange = useCallback(
|
||||
(evt) => {
|
||||
const newVal = evt.target.value
|
||||
// set validity so it will display
|
||||
valid.current = validateVal(newVal, fractions, min, max)
|
||||
|
||||
// handle the change
|
||||
debouncedHandleChange(newVal)
|
||||
},
|
||||
[debouncedHandleChange, fractions, min, max, valid]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof onMount === 'function') {
|
||||
onMount(valid.current)
|
||||
}
|
||||
}, [onMount, valid])
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
inputMode="number"
|
||||
className={`input input-secondary ${className || 'input-sm grow text-base-content'}
|
||||
${valid.current === false && 'input-error'}
|
||||
${valid.current && 'input-success'}
|
||||
`}
|
||||
value={displayVal}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** A component that shows a number input to edit a value */
|
||||
const EditCount = (props) => {
|
||||
const { handleChange } = props
|
||||
const onUpdate = useCallback(
|
||||
(validVal) => {
|
||||
if (validVal !== null && validVal !== false) handleChange(validVal)
|
||||
},
|
||||
[handleChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="form-control mb-2 w-full">
|
||||
<label className="label">
|
||||
<span className="label-text text-base-content">{props.min}</span>
|
||||
<span className="label-text font-bold text-base-content">{props.current}</span>
|
||||
<span className="label-text text-base-content">{props.max}</span>
|
||||
</label>
|
||||
<label className="input-group input-group-sm">
|
||||
<NumberInput value={props.current} onUpdate={onUpdate} min={props.min} max={props.max} />
|
||||
<span className="text-base-content font-bold">#</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to get the change handler for an input.
|
||||
* @param {Number|String|Boolean} options.dflt the default value for the input
|
||||
* @param {Function} options.updateFunc the onChange
|
||||
* @param {string} options.name the name of the property being changed
|
||||
* @return the change handler for the input
|
||||
*/
|
||||
const useSharedHandlers = ({ dflt, updateFunc, name }) => {
|
||||
return useCallback(
|
||||
(newCurrent) => {
|
||||
if (newCurrent === dflt) newCurrent = undefined
|
||||
updateFunc([name], newCurrent)
|
||||
},
|
||||
[dflt, updateFunc, name]
|
||||
)
|
||||
}
|
||||
|
||||
/** get the configuration that allows a boolean value to use the list input */
|
||||
const useBoolConfig = (name, config) => {
|
||||
return useMemo(
|
||||
() => ({
|
||||
list: [false, true],
|
||||
choiceTitles: {
|
||||
false: `${name}No`,
|
||||
true: `${name}Yes`,
|
||||
},
|
||||
valueTitles: {
|
||||
false: 'no',
|
||||
true: 'yes',
|
||||
},
|
||||
...config,
|
||||
}),
|
||||
[name, config]
|
||||
)
|
||||
}
|
||||
|
||||
/** a toggle input for list/boolean values */
|
||||
export const ListToggle = ({ config, changed, updateFunc, name }) => {
|
||||
const boolConfig = useBoolConfig(name, config)
|
||||
const handleChange = useSharedHandlers({ dflt: boolConfig.dflt, updateFunc, name })
|
||||
|
||||
const dfltIndex = boolConfig.list.indexOf(boolConfig.dflt)
|
||||
|
||||
const doToggle = () =>
|
||||
handleChange(boolConfig.list[changed ? dfltIndex : Math.abs(dfltIndex - 1)])
|
||||
|
||||
const checked = boolConfig.dflt == false ? changed : !changed
|
||||
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
className={`toggle ${changed ? 'toggle-accent' : 'toggle-secondary'}`}
|
||||
checked={checked}
|
||||
onChange={doToggle}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An input for selecting and item from a list
|
||||
* @param {String} options.name the name of the property this input changes
|
||||
* @param {Object} options.config configuration for the input
|
||||
* @param {String|Number} options.current the current value of the input
|
||||
* @param {Function} options.updateFunc the function called by the event handler to update the value
|
||||
* @param {Boolean} options.compact include descriptions with the list items?
|
||||
* @param {Function} options.t translation function
|
||||
* @param {String} design name of the design
|
||||
* @param {Boolean} isDesignOption Whether or not it's a design option
|
||||
*/
|
||||
export const ListInput = ({
|
||||
name,
|
||||
config,
|
||||
current,
|
||||
updateFunc,
|
||||
compact = false,
|
||||
t,
|
||||
changed,
|
||||
design,
|
||||
isDesignOption = false,
|
||||
}) => {
|
||||
const handleChange = useSharedHandlers({
|
||||
dflt: config.dflt,
|
||||
updateFunc,
|
||||
name,
|
||||
})
|
||||
|
||||
return config.list.map((entry) => {
|
||||
const titleKey = config.choiceTitles
|
||||
? config.choiceTitles[entry]
|
||||
: isDesignOption
|
||||
? `${design}:${name}.${entry}`
|
||||
: `${name}.o.${entry}`
|
||||
const title = config.titleMethod ? config.titleMethod(entry, t) : t(`${titleKey}.t`)
|
||||
const desc = config.valueMethod ? config.valueMethod(entry, t) : t(`${titleKey}.d`)
|
||||
const sideBySide = config.sideBySide || desc.length + title.length < 42
|
||||
|
||||
return (
|
||||
<ButtonFrame
|
||||
dense={config.dense || false}
|
||||
key={entry}
|
||||
active={
|
||||
changed
|
||||
? Array.isArray(current)
|
||||
? current.includes(entry)
|
||||
: current === entry
|
||||
: entry === config.dflt
|
||||
}
|
||||
onClick={() => handleChange(entry)}
|
||||
>
|
||||
<div
|
||||
className={`w-full flex items-start ${
|
||||
sideBySide ? 'flex-row justify-between gap-2' : 'flex-col'
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold text-lg shrink-0">{title}</div>
|
||||
{compact ? null : <div className="text-base font-normal">{desc}</div>}
|
||||
</div>
|
||||
</ButtonFrame>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/** A boolean version of {@see ListInput} that sets up the necessary configuration */
|
||||
export const BoolInput = (props) => {
|
||||
const { name, config } = props
|
||||
const boolConfig = useBoolConfig(name, config)
|
||||
|
||||
return <ListInput {...props} config={boolConfig} />
|
||||
}
|
||||
|
||||
export const useDebouncedHandlers = ({ handleChange = () => {}, val }) => {
|
||||
// hold onto what we're showing as the value so that the input doesn't look unresponsive
|
||||
const [displayVal, setDisplayVal] = useState(val)
|
||||
|
||||
// the debounce function needs to be it's own memoized value so we can flush it on unmount
|
||||
const debouncer = useMemo(
|
||||
() => debounce(handleChange, 300, { leading: true, trailing: true }),
|
||||
[handleChange]
|
||||
)
|
||||
|
||||
// this is the change handler
|
||||
const debouncedHandleChange = useCallback(
|
||||
(newVal) => {
|
||||
// always set the display
|
||||
setDisplayVal(newVal)
|
||||
// debounce the actual update
|
||||
debouncer(newVal)
|
||||
},
|
||||
[setDisplayVal, debouncer]
|
||||
)
|
||||
|
||||
// immediately call the debounced function on unmount so we don't miss an update
|
||||
useEffect(() => debouncer.flush, [debouncer])
|
||||
|
||||
// set the display val to the current value when it gets changed
|
||||
useEffect(() => {
|
||||
setDisplayVal(val)
|
||||
}, [val])
|
||||
|
||||
return { debouncedHandleChange, displayVal }
|
||||
}
|
||||
|
||||
/**
|
||||
* An input component that uses a slider to change a number value
|
||||
* @param {String} options.name the name of the property being changed by the input
|
||||
* @param {Object} options.config configuration for the input
|
||||
* @param {Number} options.current the current value of the input
|
||||
* @param {Function} options.updateFunc the function called by the event handler to update the value
|
||||
* @param {Function} options.t translation function
|
||||
* @param {Boolean} options.override open the text input to allow override of the slider?
|
||||
* @param {String} options.suffix a suffix to append to value labels
|
||||
* @param {Function} options.valFormatter a function that accepts a value and formats it for display as a label
|
||||
* @param {Function} options.setReset a setter for the reset function on the parent component
|
||||
*/
|
||||
export const SliderInput = ({
|
||||
name,
|
||||
config,
|
||||
current,
|
||||
updateFunc,
|
||||
t,
|
||||
override,
|
||||
suffix = '',
|
||||
valFormatter = (val) => val,
|
||||
setReset,
|
||||
children,
|
||||
changed,
|
||||
}) => {
|
||||
const { max, min } = config
|
||||
const handleChange = useSharedHandlers({
|
||||
current,
|
||||
dflt: config.dflt,
|
||||
updateFunc,
|
||||
name,
|
||||
setReset,
|
||||
})
|
||||
|
||||
const { debouncedHandleChange, displayVal } = useDebouncedHandlers({
|
||||
handleChange,
|
||||
val: changed ? current : config.dflt,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row justify-between">
|
||||
{override ? (
|
||||
<EditCount
|
||||
{...{
|
||||
current: displayVal,
|
||||
handleChange,
|
||||
min,
|
||||
max,
|
||||
t,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span className="opacity-50">
|
||||
<span dangerouslySetInnerHTML={{ __html: valFormatter(min) + suffix }} />
|
||||
</span>
|
||||
<span
|
||||
className={`font-bold ${
|
||||
displayVal === config.dflt ? 'text-secondary' : 'text-accent'
|
||||
}`}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: valFormatter(displayVal) + suffix }} />
|
||||
</span>
|
||||
<span className="opacity-50">
|
||||
<span dangerouslySetInnerHTML={{ __html: valFormatter(max) + suffix }} />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
{...{ min, max, value: displayVal, step: config.step || 0.1 }}
|
||||
onChange={(evt) => debouncedHandleChange(evt.target.value)}
|
||||
className={`
|
||||
range range-sm mt-1
|
||||
${changed ? 'range-accent' : 'range-secondary'}
|
||||
`}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* round a value to the correct number of decimal places to display all supplied digits after multiplication
|
||||
* this is a workaround for floating point errors
|
||||
* examples:
|
||||
* roundPct(0.72, 100) === 72
|
||||
* roundPct(7.5, 0.01) === 0.075
|
||||
* roundPct(7.50, 0.01) === 0.0750
|
||||
* @param {Number} num the number to be operated on
|
||||
* @param {Number} factor the number to multiply by
|
||||
* @return {Number} the given num multiplied by the factor, rounded appropriately
|
||||
*/
|
||||
const roundPct = (num, factor) => {
|
||||
// stringify
|
||||
const str = '' + num
|
||||
// get the index of the decimal point in the number
|
||||
const decimalIndex = str.indexOf('.')
|
||||
// get the number of places the factor moves the decimal point
|
||||
const factorPlaces = factor > 0 ? Math.ceil(Math.log10(factor)) : Math.floor(Math.log10(factor))
|
||||
// the number of places needed is the number of digits that exist after the decimal minus the number of places the decimal point is being moved
|
||||
const numPlaces = Math.max(0, str.length - (decimalIndex + factorPlaces))
|
||||
return round(num * factor, numPlaces)
|
||||
}
|
||||
|
||||
/** A {@see SliderInput} to handle percentage values */
|
||||
export const PctInput = ({ current, changed, updateFunc, config, ...rest }) => {
|
||||
const factor = 100
|
||||
let pctCurrent = changed ? roundPct(current, factor) : current
|
||||
const pctUpdateFunc = useCallback(
|
||||
(path, newVal) =>
|
||||
updateFunc(path, newVal === undefined ? undefined : roundPct(newVal, 1 / factor)),
|
||||
[updateFunc]
|
||||
)
|
||||
|
||||
return (
|
||||
<SliderInput
|
||||
{...{
|
||||
...rest,
|
||||
config: { ...config, dflt: roundPct(config.dflt, factor) },
|
||||
current: pctCurrent,
|
||||
updateFunc: pctUpdateFunc,
|
||||
suffix: '%',
|
||||
valFormatter: round,
|
||||
changed,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** A {@see SliderInput} to handle degree values */
|
||||
export const DegInput = (props) => {
|
||||
const { updateFunc } = props
|
||||
const degUpdateFunc = useCallback(
|
||||
(path, newVal) => {
|
||||
updateFunc(path, newVal === undefined ? undefined : Number(newVal))
|
||||
},
|
||||
[updateFunc]
|
||||
)
|
||||
return <SliderInput {...props} suffix="°" valFormatter={round} updateFunc={degUpdateFunc} />
|
||||
}
|
||||
|
||||
export const MmInput = (props) => {
|
||||
const { units, updateFunc, current, config } = props
|
||||
const mmUpdateFunc = useCallback(
|
||||
(path, newCurrent) => {
|
||||
const calcCurrent =
|
||||
typeof newCurrent === 'undefined' ? undefined : measurementAsMm(newCurrent, units)
|
||||
updateFunc(path, calcCurrent)
|
||||
},
|
||||
[updateFunc, units]
|
||||
)
|
||||
|
||||
// add a default step that's appropriate to the unit. can be overwritten by config
|
||||
const defaultStep = units === 'imperial' ? 0.125 : 0.1
|
||||
|
||||
return (
|
||||
<SliderInput
|
||||
{...{
|
||||
...props,
|
||||
config: {
|
||||
step: defaultStep,
|
||||
...config,
|
||||
dflt: measurementAsUnits(config.dflt, units),
|
||||
},
|
||||
current: current === undefined ? undefined : measurementAsUnits(current, units),
|
||||
updateFunc: mmUpdateFunc,
|
||||
valFormatter: (val) => (units === 'imperial' ? formatFraction128(val, null) : val),
|
||||
suffix: units === 'imperial' ? '"' : 'cm',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** A placeholder for an input to handle constant values */
|
||||
export const ConstantInput = ({
|
||||
type = 'number',
|
||||
name,
|
||||
current,
|
||||
updateFunc,
|
||||
//t,
|
||||
changed,
|
||||
config,
|
||||
}) => (
|
||||
<>
|
||||
<input
|
||||
type={type}
|
||||
className={`
|
||||
input input-bordered w-full text-base-content
|
||||
input-${changed ? 'secondary' : 'accent'}
|
||||
`}
|
||||
value={changed ? current : config.dflt}
|
||||
onChange={(evt) => updateFunc([name], evt.target.value)}
|
||||
/>
|
||||
</>
|
||||
)
|
278
sites/shared/components/workbench/menus/shared/menu-item.mjs
Normal file
278
sites/shared/components/workbench/menus/shared/menu-item.mjs
Normal file
|
@ -0,0 +1,278 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { ResetIcon, EditIcon } from 'shared/components/icons.mjs'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { SubAccordion } from 'shared/components/accordion.mjs'
|
||||
import { FormControl } from 'shared/components/inputs.mjs'
|
||||
import { BoxIcon as GroupIcon, OptionsIcon } from 'shared/components/icons.mjs'
|
||||
import { optionType } from 'shared/utils.mjs'
|
||||
|
||||
/**
|
||||
* Check to see if a value is different from its default
|
||||
* @param {Number|String|Boolean} current the current value
|
||||
* @param {Object} config configuration containing a dflt key
|
||||
* @return {Boolean} was the value changed?
|
||||
*/
|
||||
export const wasChanged = (current, config) => {
|
||||
if (typeof current === 'undefined') return false
|
||||
if (current == config.dflt) return false
|
||||
|
||||
return true
|
||||
}
|
||||
/**
|
||||
* A generic component to present the title of a menu item
|
||||
* @param {String} options.name the name of the item, to act as its translation key
|
||||
* @param {Function} options.t the translation function
|
||||
* @param {String|React.Component} options.current a the current value, or a Value component to display it
|
||||
* @param {Boolean} options.open is the menu item open?
|
||||
* @param {String} options.emoji the emoji icon of the menu item
|
||||
*/
|
||||
export const ItemTitle = ({ name, t, current = null, open = false, emoji = '', Icon = false }) => (
|
||||
<div className={`flex flex-row gap-1 items-center w-full ${open ? '' : 'justify-between'}`}>
|
||||
<span className="font-medium capitalize flex flex-row gap-2">
|
||||
{Icon ? <Icon /> : <span role="img">{emoji}</span>}
|
||||
{t([`${name}.t`, name])}
|
||||
</span>
|
||||
<span className="font-bold">{current}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
/** @type {String} class to apply to buttons on open menu items */
|
||||
const iconButtonClass = 'btn btn-xs btn-ghost px-0 text-accent'
|
||||
|
||||
/**
|
||||
* A generic component for handling a menu item.
|
||||
* Wraps the given input in a {@see Collapse} with the appropriate buttons
|
||||
* @param {String} options.name the name of the item, for using as a key
|
||||
* @param {Object} options.config the configuration for the input
|
||||
* @param {Sting|Boolean|Number} options.current the current value of the item
|
||||
* @param {Function} options.updateFunc the function that will be called by event handlers to update the value
|
||||
* @param {Function} options.t the translation function
|
||||
* @param {Object} options.passProps props to pass to the Input component
|
||||
* @param {Boolean} changed has the value changed from default?
|
||||
* @param {React.Component} Input the input component this menu item will use
|
||||
* @param {React.Component} Value a value display component this menu item will use
|
||||
* @param {Boolean} allowOverride all a text input to be used to override the given input component
|
||||
* @param {Number} control the user-defined control level
|
||||
*/
|
||||
export const MenuItem = ({
|
||||
name,
|
||||
config,
|
||||
current,
|
||||
updateFunc,
|
||||
t,
|
||||
passProps = {},
|
||||
changed,
|
||||
Input = () => {},
|
||||
allowOverride = false,
|
||||
control = Infinity,
|
||||
docs,
|
||||
design,
|
||||
}) => {
|
||||
// state for knowing whether the override input should be shown
|
||||
const [override, setOverride] = useState(false)
|
||||
|
||||
// generate properties to pass to the Input
|
||||
const drillProps = useMemo(
|
||||
() => ({
|
||||
name,
|
||||
config,
|
||||
control,
|
||||
current,
|
||||
updateFunc,
|
||||
t,
|
||||
changed,
|
||||
override,
|
||||
design,
|
||||
...passProps,
|
||||
}),
|
||||
[name, config, current, updateFunc, t, changed, override, passProps, control]
|
||||
)
|
||||
|
||||
// don't render if this item is more advanced than the user has chosen to see
|
||||
if (config.control && config.control > control) return null
|
||||
|
||||
// get buttons for open and closed states
|
||||
const buttons = []
|
||||
if (allowOverride)
|
||||
buttons.push(
|
||||
<button
|
||||
key="edit"
|
||||
className={iconButtonClass}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation()
|
||||
setOverride(!override)
|
||||
}}
|
||||
>
|
||||
<EditIcon
|
||||
className={`w-6 h-6 ${
|
||||
override ? 'bg-secondary text-secondary-content rounded' : 'text-secondary'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
const ResetButton = ({ disabled = false }) => (
|
||||
<button
|
||||
className={`${iconButtonClass} disabled:bg-opacity-0`}
|
||||
disabled={disabled}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation()
|
||||
updateFunc([name])
|
||||
}}
|
||||
>
|
||||
<ResetIcon />
|
||||
</button>
|
||||
)
|
||||
|
||||
buttons.push(<ResetButton open disabled={!changed} key="clear" />)
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
label={<span className="text-base font-normal">{t([`${name}.d`, name])}</span>}
|
||||
id={config.name}
|
||||
labelBR={<div className="flex flex-row items-center gap-2">{buttons}</div>}
|
||||
labelBL={
|
||||
<span
|
||||
className={`text-base font-medium -mt-2 block ${changed ? 'text-accent' : 'opacity-50'}`}
|
||||
>
|
||||
{t(`workbench:youUse${changed ? 'Default' : 'Custom'}Value`)}
|
||||
</span>
|
||||
}
|
||||
docs={docs}
|
||||
>
|
||||
<Input {...drillProps} />
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for recursively displaying groups of menu items.
|
||||
* Accepts any object where menu item configurations are keyed by name
|
||||
* Items that are group headings are expected to have an isGroup: true property
|
||||
* @param {Boolean} options.collapsible Should this group be collapsible (use false for the top level of a menu)
|
||||
* @param {Number} options.control the user-defined control level
|
||||
* @param {String} options.name the name of the group or item
|
||||
* @param {Object} options.currentValues a map of current values for items in the group, keyed by name
|
||||
* @param {Object} structure the configuration for the group.
|
||||
* @param {React.Component} Icon the icon to display next to closed groups
|
||||
* @param {React.Component} Item the component to use for menu items
|
||||
* @param {Object} values a map of Value display components to be used by menu items in the group
|
||||
* @param {Object} inputs a map of Input components to be used by menu items in the group
|
||||
* @param {Object} passProps properties to pass to Inputs within menu items
|
||||
* @param {Object} emojis a map of emojis to use as icons for groups or items
|
||||
* @param {Function} updateFunc the function called by change handlers on inputs within menu items
|
||||
* @param {Boolean} topLevel is this group the top level group? false for nested
|
||||
* @param {Function} t translation function
|
||||
*/
|
||||
export const MenuItemGroup = ({
|
||||
collapsible = true,
|
||||
control,
|
||||
//name,
|
||||
currentValues = {},
|
||||
structure,
|
||||
Icon,
|
||||
Item = MenuItem,
|
||||
values = {},
|
||||
inputs = {},
|
||||
passProps = {},
|
||||
emojis = {},
|
||||
updateFunc,
|
||||
topLevel = false,
|
||||
t,
|
||||
language,
|
||||
isDesignOptionsGroup = false,
|
||||
docs = false,
|
||||
design,
|
||||
}) => {
|
||||
// map the entries in the structure
|
||||
const content = Object.entries(structure).map(([itemName, item]) => {
|
||||
// if it's the isGroup property, or it is false, it shouldn't be shown
|
||||
if (itemName === 'isGroup' || item === false) return null
|
||||
if (!item) return null
|
||||
if (item.control && control && item.control > control) return null
|
||||
|
||||
const ItemIcon = item.icon
|
||||
? item.icon
|
||||
: item.isGroup
|
||||
? GroupIcon
|
||||
: Icon
|
||||
? Icon
|
||||
: () => <span role="img">fixme-icon</span>
|
||||
const Value = item.isGroup
|
||||
? () => (
|
||||
<div className="flex flex-row gap-2 items-center font-medium">
|
||||
{Object.keys(item).filter((i) => i !== 'isGroup').length}
|
||||
<OptionsIcon className="w-5 h-5" />
|
||||
</div>
|
||||
)
|
||||
: isDesignOptionsGroup
|
||||
? values[optionType(item)]
|
||||
: values[itemName]
|
||||
? values[itemName]
|
||||
: () => <span>¯\_(ツ)_/¯</span>
|
||||
|
||||
return [
|
||||
<div className="flex flex-row items-center justify-between w-full" key="a">
|
||||
<div className="flex flex-row items-center gap-4 w-full">
|
||||
<ItemIcon />
|
||||
<span className="font-medium">{t([`${itemName}.t`, `workbench:${itemName}`])}</span>
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
<Value
|
||||
current={currentValues[itemName]}
|
||||
config={item}
|
||||
t={t}
|
||||
changed={wasChanged(currentValues[itemName], item)}
|
||||
design={design}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
item.isGroup ? (
|
||||
<MenuItemGroup
|
||||
key={itemName}
|
||||
{...{
|
||||
collapsible: true,
|
||||
// it's the top level if the previous level was top but not wrapped
|
||||
topLevel: topLevel && !collapsible,
|
||||
control,
|
||||
name: itemName,
|
||||
currentValues,
|
||||
structure: item,
|
||||
Icon,
|
||||
Item,
|
||||
values,
|
||||
inputs,
|
||||
passProps,
|
||||
emojis,
|
||||
updateFunc,
|
||||
t,
|
||||
language,
|
||||
isDesignOptionsGroup,
|
||||
design,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Item
|
||||
key={itemName}
|
||||
{...{
|
||||
name: itemName,
|
||||
current: currentValues[itemName],
|
||||
config: item,
|
||||
control,
|
||||
changed: wasChanged(currentValues[itemName], item),
|
||||
Value: values[itemName],
|
||||
Input: inputs[itemName],
|
||||
t,
|
||||
updateFunc,
|
||||
passProps,
|
||||
language,
|
||||
docs,
|
||||
design,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
itemName,
|
||||
]
|
||||
})
|
||||
|
||||
return <SubAccordion items={content.filter((item) => item !== null)} />
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useMemo } from 'react'
|
||||
import { WrenchIcon } from 'shared/components/icons.mjs'
|
||||
import { useMobileMenu } from 'shared/context/mobile-menubar-context.mjs'
|
||||
|
||||
const defaultClasses = `w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-full overflow-scroll`
|
||||
|
||||
/**
|
||||
* a wrapper that displays its contents normally on larger screens
|
||||
* and adds its contents as a menu on the {@link MobileMenubar} for smaller screens
|
||||
* @param {ReactChildren} children
|
||||
* @param {String} wrapperClass - classes to add to the wrapper for larger screens
|
||||
* @param {ReactComponent} Icon - Icon for the menu button on smaller screens
|
||||
* @param {Boolean} [keepOpenOnClick=true] - should the modal for this menu stay open when clicked?
|
||||
* @param {String} [type='settings'] - the type of menu this is. will be used to key the menu in the MobileMenubar
|
||||
* @param {Number} [order=-1] - the order of this menu in the MobileMenubar
|
||||
*/
|
||||
export const MenuWrapper = ({
|
||||
children,
|
||||
wrapperClass = defaultClasses,
|
||||
Icon = WrenchIcon,
|
||||
keepOpenOnClick = true,
|
||||
type = 'settings',
|
||||
order = -1,
|
||||
}) => {
|
||||
const menuProps = useMemo(
|
||||
() => ({
|
||||
Icon,
|
||||
menuContent: children,
|
||||
keepOpenOnClick,
|
||||
order,
|
||||
}),
|
||||
[Icon, children, keepOpenOnClick, order]
|
||||
)
|
||||
|
||||
useMobileMenu(type, menuProps)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`hidden lg:block ${wrapperClass}`}>{children}</div>
|
||||
</>
|
||||
)
|
||||
}
|
63
sites/shared/components/workbench/menus/shared/values.mjs
Normal file
63
sites/shared/components/workbench/menus/shared/values.mjs
Normal file
|
@ -0,0 +1,63 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { formatMm } from 'shared/utils.mjs'
|
||||
import { BoolYesIcon, BoolNoIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
/*********************************************************************************************************
|
||||
* This file contains the base components to be used for displaying values in menu titles in the workbench
|
||||
* Values that deal with more specific use cases should wrap one of the below components
|
||||
*********************************************************************************************************/
|
||||
|
||||
/** The basis of it all. Handles the changed/unchanged styling for the wrapped value */
|
||||
export const HighlightedValue = ({ changed, children }) => (
|
||||
<span className={changed ? 'text-accent' : ''}> {children} </span>
|
||||
)
|
||||
|
||||
/**
|
||||
* A wrapper for displaying the correct value based on whether or not the value has changed
|
||||
* @param {Number|String|Boolean} options.current the current value, if it has been changed
|
||||
* @param {Number|String|Boolean} options.dflt the default value
|
||||
* @param {Boolean} options.changed has the value been changed?
|
||||
*/
|
||||
export const PlainValue = ({ current, dflt, changed }) => (
|
||||
<HighlightedValue changed={changed}> {changed ? current : dflt} </HighlightedValue>
|
||||
)
|
||||
|
||||
/**
|
||||
* Displays the correct, translated value for a list
|
||||
* @param {String|Boolean} options.current the current value, if it has been changed
|
||||
* @param {Function} options.t a translation function
|
||||
* @param {Object} options.config the item config
|
||||
* @param {Boolean} options.changed has the value been changed?
|
||||
*/
|
||||
export const ListValue = ({ current, t, config, changed }) => {
|
||||
// get the values
|
||||
const val = changed ? current : config.dflt
|
||||
|
||||
// key will be based on a few factors
|
||||
let key
|
||||
// are valueTitles configured?
|
||||
if (config.valueTitles) key = config.valueTitles[val]
|
||||
// if not, is the value a string
|
||||
else if (typeof val === 'string') key = val
|
||||
// otherwise stringify booleans
|
||||
else if (val) key = <BoolYesIcon />
|
||||
else key = <BoolNoIcon />
|
||||
|
||||
const translated = config.doNotTranslate || typeof key !== 'string' ? key : t(key)
|
||||
|
||||
return <HighlightedValue changed={changed}>{translated}</HighlightedValue>
|
||||
}
|
||||
|
||||
/** Displays the corrent, translated value for a boolean */
|
||||
export const BoolValue = ListValue
|
||||
|
||||
/** Displays a formated mm value based on the current units */
|
||||
export const MmValue = ({ current, config, units, changed }) => (
|
||||
<HighlightedValue changed={changed}>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatMm(changed ? current : config.dflt, units),
|
||||
}}
|
||||
/>
|
||||
</HighlightedValue>
|
||||
)
|
|
@ -0,0 +1,47 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { RocketIcon, ControlIcon, KioskIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const loadSettingsConfig = () => {
|
||||
const uiSettings = {
|
||||
control: {
|
||||
control: 1, // Show when control > 0
|
||||
emoji: '🖥️',
|
||||
list: [1, 2, 3, 4, 5],
|
||||
choiceTitles: {},
|
||||
icon: ControlIcon,
|
||||
},
|
||||
kiosk: {
|
||||
control: 4, // Show when control > 3
|
||||
list: [0, 1],
|
||||
choiceTitles: {
|
||||
0: 'ui-settings:websiteMode',
|
||||
1: 'ui-settings:kioskMode',
|
||||
},
|
||||
//valueTitles: {
|
||||
// react: 'ui-settings:regular',
|
||||
// svg: 'ui-settings:kiosk',
|
||||
//},
|
||||
dflt: 0,
|
||||
icon: KioskIcon,
|
||||
},
|
||||
renderer: {
|
||||
control: 4, // Show when control > 3
|
||||
list: ['react', 'svg'],
|
||||
choiceTitles: {
|
||||
react: 'ui-settings:renderWithReact',
|
||||
svg: 'ui-settings:renderWithCore',
|
||||
},
|
||||
valueTitles: {
|
||||
react: 'React',
|
||||
svg: 'SVG',
|
||||
},
|
||||
dflt: 'react',
|
||||
icon: RocketIcon,
|
||||
},
|
||||
}
|
||||
|
||||
uiSettings.control.list.forEach(
|
||||
(i) => (uiSettings.control.choiceTitles[i] = 'account:control' + i)
|
||||
)
|
||||
return uiSettings
|
||||
}
|
43
sites/shared/components/workbench/menus/ui-settings/de.yaml
Normal file
43
sites/shared/components/workbench/menus/ui-settings/de.yaml
Normal file
|
@ -0,0 +1,43 @@
|
|||
uiSettings.t: UI Präferenzen
|
||||
uiSettings.d: Diese Einstellungen steuern die UI-Aspekte (Benutzeroberfläche) unserer Online-Umgebung für den Entwurf von Mustern.
|
||||
renderer.t: Rendering Engine
|
||||
renderer.d: Legt fest, wie das Muster auf dem Bildschirm gerendert (gezeichnet) wird
|
||||
renderWithReact.t: Rendern mit den React-Komponenten von FreeSewing
|
||||
renderWithReact.d: Rendere als SVG durch unsere React-Komponenten. Ermöglicht Interaktivität und ist für den Bildschirm optimiert. Verwende sie, wenn du dir nicht sicher bist, was du wählen sollst.
|
||||
renderWithCore.t: Rendering mit der Core-Bibliothek von Freesewing
|
||||
renderWithCore.d: Rendere direkt aus Core in SVG. Erlaubt keine Interaktivität und ist für den Druck optimiert. Verwende dies, wenn du wissen willst, wie es nach dem Export aussehen wird.
|
||||
control.t: Benutzererfahrung
|
||||
control.d: Welches Benutzererlebnis bevorzugst du? Bitte beachte, dass dies eine Kontoeinstellung ist, die sich auf die gesamte Website auswirkt.
|
||||
inspect.t: Überprüfe
|
||||
inspect.d: Wenn du diese Funktion aktivierst, kannst du das Muster aufschlüsseln und Informationen über seine verschiedenen Teile, Pfade und Punkte abrufen.
|
||||
inspectNo.t: Deaktiviere den Inspektor
|
||||
inspectNo.d: Das ist die Standardeinstellung, der Musterinspektor ist deaktiviert und das Muster wird wie gewohnt angezeigt.
|
||||
inspectYes.t: Aktiviere den Inspektor
|
||||
inspectYes.d: Wenn der Musterinspektor aktiviert und die React-Rendering-Engine ausgewählt ist, fügen wir dem Muster Interaktivität hinzu, damit du die verschiedenen Elemente, aus denen das Muster besteht, untersuchen kannst.
|
||||
no: No
|
||||
yes: Yes
|
||||
draft: Entwurf
|
||||
test: Test
|
||||
print: Layout drucken
|
||||
cut: Layout schneiden
|
||||
save: Speichern
|
||||
export: Exportieren
|
||||
edit: bearbeiten
|
||||
draft.t: Zeichne dein Schnittmuster
|
||||
draft.d: Einführung des FreeSewing Muster-Editors, mit dem du dein Muster nach Herzenslust verändern kannst
|
||||
test.t: Teste dein Schnittmuster
|
||||
test.d: Sieh dir an, wie sich dein Muster an Änderungen der Optionen oder Maße anpasst
|
||||
print.t: Layout drucken
|
||||
print.d: Ermöglicht es dir, deine Musterteile so anzuordnen, dass du dein Muster auf so wenig Seiten wie möglich drucken kannst
|
||||
cut.t: Layout schneiden
|
||||
cut.d: Damit kannst du deine Schnittmusterteile so anordnen, dass du genau bestimmen kannst, wie viel Stoff du für die Herstellung brauchst.
|
||||
save.t: Speichere dein Muster
|
||||
save.d: Speichere das aktuelle Muster in deinem FreeSewing-Konto
|
||||
export.t: Exportiere dein Muster
|
||||
export.d: Ermöglicht es dir, dieses Muster in eine Vielzahl von Formaten zu exportieren
|
||||
logs.t: Musterprotokolle
|
||||
logs.d: Ermöglicht es dir, die Musterprotokolle zu durchsuchen, um zu sehen, was genau beim Entwerfen dieses Musters passiert ist
|
||||
edit.t: Bearbeiten Sie die Mustereinstellungen von Hand
|
||||
edit.d: So kannst du die Mustereinstellungen von Hand bearbeiten und hast die volle Kontrolle darüber, wie dein Muster gezeichnet wird.
|
||||
view: Siehe
|
||||
view.d: Dies sind die verschiedenen Ansichten, aus denen du wählen kannst. Dazu gehören die Ansichten, die über die Navigationsleiste oder das Menü verfügbar sind, sowie einige zusätzliche Ansichten
|
43
sites/shared/components/workbench/menus/ui-settings/en.yaml
Normal file
43
sites/shared/components/workbench/menus/ui-settings/en.yaml
Normal file
|
@ -0,0 +1,43 @@
|
|||
uiSettings.t: UI Preferences
|
||||
uiSettings.d: These preferences control the UI (User Interface) aspects of our online pattern drafting environment.
|
||||
renderer.t: Render Engine
|
||||
renderer.d: Controls how the pattern is rendered (drawn) on the screen
|
||||
renderWithReact.t: Render with FreeSewing's React components
|
||||
renderWithReact.d: Render as SVG through our React components. Allows interactivity and is optimized for screen. Use this if you are not sure what to pick.
|
||||
renderWithCore.t: Render with Freesewing's Core library
|
||||
renderWithCore.d: Render directly to SVG from Core. Allows no interactivity and is optimized for print. Use this if you want to know what it will look like when exported.
|
||||
control.t: User Experience
|
||||
control.d: Which user experience do you prefer? Please note that this is an account setting, so it will impact the entire website.
|
||||
inspect.t: Inspect
|
||||
inspect.d: Enabling this will allow you to drill down into the pattern, and pull up information about its various parts, paths, and points.
|
||||
inspectNo.t: Disable the inspector
|
||||
inspectNo.d: This is the default, the pattern inspector is disabled and the pattern is displayed as usual.
|
||||
inspectYes.t: Enable the inspector
|
||||
inspectYes.d: With the pattern inspector enabled and the React rendering engine selected, we will add interactivity to the pattern to allow you to inspect the various elements that make up the pattern.
|
||||
no: No
|
||||
yes: Yes
|
||||
draft: Draft
|
||||
test: Test
|
||||
print: Print layout
|
||||
cut: Cut Layout
|
||||
save: Save
|
||||
export: Export
|
||||
edit: Edit
|
||||
draft.t: Draft your pattern
|
||||
draft.d: Launches FreeSewing flagship pattern editor, where you can tweak your pattern to your heart's desire
|
||||
test.t: Test your pattern
|
||||
test.d: See how your pattern adapts to changes in options, or measurements
|
||||
print.t: Print Layout
|
||||
print.d: Allows you to arrange your pattern pieces so you can printing your pattern on as little pages as possible
|
||||
cut.t: Cutting layout
|
||||
cut.d: Allows you to arrange your pattern pieces so you can determine exactly how much fabric you need to make it.
|
||||
save.t: Save your pattern
|
||||
save.d: Save the current pattern to your FreeSewing account
|
||||
export.t: Export your pattern
|
||||
export.d: Allows you to export this pattern to a variety of formats
|
||||
logs.t: Pattern logs
|
||||
logs.d: Allows you to browse the pattern logs to see what exactly happened while drafting this pattern
|
||||
edit.t: Hand-edit the pattern settings
|
||||
edit.d: This allows you to hand-edit the pattern settings, giving you full control over how your pattern will be drafted
|
||||
view: View
|
||||
view.d: These are the various views you can pick from. Includes those views available via the navigation bar or menu, and some additional ones
|
43
sites/shared/components/workbench/menus/ui-settings/es.yaml
Normal file
43
sites/shared/components/workbench/menus/ui-settings/es.yaml
Normal file
|
@ -0,0 +1,43 @@
|
|||
uiSettings.t: Preferencias de IU
|
||||
uiSettings.d: Estas preferencias controlan los aspectos de la IU (Interfaz de Usuario) de nuestro entorno de elaboración de patrones en línea.
|
||||
renderer.t: Motor de renderizado
|
||||
renderer.d: Controla cómo se representa (dibuja) el patrón en la pantalla
|
||||
renderWithReact.t: Renderiza con los componentes React de FreeSewing
|
||||
renderWithReact.d: Renderiza como SVG a través de nuestros componentes React. Permite la interactividad y está optimizado para la pantalla. Utilízalo si no estás seguro de qué elegir.
|
||||
renderWithCore.t: Renderiza con la biblioteca Core de Freesewing
|
||||
renderWithCore.d: Renderiza directamente a SVG desde Core. No permite interactividad y está optimizado para impresión. Utilízalo si quieres saber qué aspecto tendrá cuando se exporte.
|
||||
control.t: Experiencia del usuario
|
||||
control.d: '¿Qué experiencia de usuario prefieres? Ten en cuenta que se trata de una configuración de cuenta, por lo que afectará a todo el sitio web.'
|
||||
inspect.t: Inspecciona
|
||||
inspect.d: Si activas esta opción, podrás profundizar en el patrón y obtener información sobre sus distintas partes, rutas y puntos.
|
||||
inspectNo.t: Desactivar el inspector
|
||||
inspectNo.d: Este es el valor por defecto, el inspector de patrones está desactivado y el patrón se muestra como de costumbre.
|
||||
inspectYes.t: Activar el inspector
|
||||
inspectYes.d: Con el inspector de patrones activado y el motor de renderizado React seleccionado, añadiremos interactividad al patrón para que puedas inspeccionar los distintos elementos que lo componen.
|
||||
no: No
|
||||
yes: Yes
|
||||
draft: Boceto
|
||||
test: Prueba
|
||||
print: Diseño de impresión
|
||||
cut: Disposición del corte
|
||||
save: Guardar
|
||||
export: Exportar
|
||||
edit: Edita
|
||||
draft.t: Traza tu patrón
|
||||
draft.d: Lanza el editor de patrones insignia de FreeSewing, donde puedes retocar tu patrón a tu antojo
|
||||
test.t: Prueba tu patrón
|
||||
test.d: Comprueba cómo se adapta tu patrón a los cambios de opciones o medidas
|
||||
print.t: Diseño de impresión
|
||||
print.d: Te permite organizar las piezas de tu patrón para que puedas imprimirlo en el menor número de páginas posible
|
||||
cut.t: Disposición de corte
|
||||
cut.d: Te permite organizar las piezas de tu patrón para que puedas determinar exactamente cuánta tela necesitas para confeccionarlo.
|
||||
save.t: Guarda tu patrón
|
||||
save.d: Guarda el patrón actual en tu cuenta de FreeSewing
|
||||
export.t: Exporta tu patrón
|
||||
export.d: Te permite exportar este patrón a varios formatos
|
||||
logs.t: Registros de patrones
|
||||
logs.d: Te permite examinar los registros del patrón para ver qué ocurrió exactamente mientras se redactaba este patrón
|
||||
edit.t: Edita a mano los ajustes del patrón
|
||||
edit.d: Esto te permite editar a mano los ajustes del patrón, dándote un control total sobre cómo se redactará tu patrón.
|
||||
view: Ver
|
||||
view.d: Estas son las distintas vistas que puedes elegir. Incluye las vistas disponibles a través de la barra de navegación o el menú, y algunas adicionales
|
43
sites/shared/components/workbench/menus/ui-settings/fr.yaml
Normal file
43
sites/shared/components/workbench/menus/ui-settings/fr.yaml
Normal file
|
@ -0,0 +1,43 @@
|
|||
uiSettings.t: Préférences de l'interface utilisateur
|
||||
uiSettings.d: Ces préférences contrôlent les aspects de l'interface utilisateur de notre environnement de création de patrons en ligne.
|
||||
renderer.t: Moteur de Rendu
|
||||
renderer.d: Contrôle comment le patron est restitué (dessiné) à l'écran
|
||||
renderWithReact.t: Rendu avec les composants React de FreeSewing
|
||||
renderWithReact.d: Rendu en tant que SVG par l'intermédiaire de nos composants React. Permet l'interactivité et est optimisé pour l'écran. Utilise ceci si tu n'es pas sûr de ce que tu dois choisir.
|
||||
renderWithCore.t: Rendu avec la bibliothèque Core de Freesewing
|
||||
renderWithCore.d: Rendu directement en SVG à partir de Core. Ne permet aucune interactivité et est optimisé pour l'impression. Utilise-le si tu veux savoir à quoi il ressemblera une fois exporté.
|
||||
control.t: Expérience de l'utilisateur
|
||||
control.d: Quelle expérience utilisateur préfères-tu ? Note qu'il s'agit d'un paramètre de compte, il aura donc un impact sur l'ensemble du site Web.
|
||||
inspect.t: Inspecter
|
||||
inspect.d: L'activation de cette fonction te permettra d'approfondir le modèle et d'obtenir des informations sur ses différentes parties, ses chemins et ses points.
|
||||
inspectNo.t: Désactive l'inspecteur
|
||||
inspectNo.d: C'est la valeur par défaut, l'inspecteur de motifs est désactivé et le motif s'affiche comme d'habitude.
|
||||
inspectYes.t: Active l'inspecteur
|
||||
inspectYes.d: Avec l'inspecteur de motifs activé et le moteur de rendu React sélectionné, nous allons ajouter de l'interactivité au motif pour te permettre d'inspecter les différents éléments qui le composent.
|
||||
no: No
|
||||
yes: Yes
|
||||
draft: Ébauche
|
||||
test: Test
|
||||
print: Mise en page d'impression
|
||||
cut: Disposition des coupes
|
||||
save: Sauvegarder
|
||||
export: Exporter
|
||||
edit: Éditer
|
||||
draft.t: Dessiner votre patron
|
||||
draft.d: Lance l'éditeur de patrons phare de FreeSewing, où tu peux modifier tes patrons selon tes désirs.
|
||||
test.t: Tester votre patron
|
||||
test.d: Vois comment ton modèle s'adapte aux changements d'options, ou de mesures.
|
||||
print.t: Mise en page d'impression
|
||||
print.d: Permet de disposer les pièces de ton patron de façon à ce que tu puisses imprimer ton patron sur le moins de pages possible.
|
||||
cut.t: Plan de coupe
|
||||
cut.d: Permet de disposer les pièces de ton patron de façon à déterminer exactement la quantité de tissu dont tu as besoin pour le réaliser.
|
||||
save.t: Sauvegarde ton modèle
|
||||
save.d: Sauvegarde le modèle actuel sur ton compte FreeSewing.
|
||||
export.t: Exporte ton modèle
|
||||
export.d: Permet d'exporter ce motif dans différents formats.
|
||||
logs.t: Fiches d'information sur les modèles
|
||||
logs.d: Permet de parcourir les journaux des patrons pour voir ce qui s'est passé exactement lors de la réalisation de ce patron.
|
||||
edit.t: Modifie à la main les paramètres du modèle
|
||||
edit.d: Cela te permet d'éditer à la main les paramètres du modèle, ce qui te donne un contrôle total sur la façon dont ton modèle sera dessiné
|
||||
view: Voir
|
||||
view.d: Voici les différentes vues que tu peux choisir. Comprend les vues disponibles via la barre de navigation ou le menu, ainsi que quelques vues supplémentaires.
|
|
@ -0,0 +1,54 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
//Dependencies
|
||||
import { loadSettingsConfig } from './config.mjs'
|
||||
// Components
|
||||
import { WorkbenchMenu } from '../shared/index.mjs'
|
||||
import { MenuItem } from '../shared/menu-item.mjs'
|
||||
import { DesktopIcon } from 'shared/components/icons.mjs'
|
||||
import { inputs } from './inputs.mjs'
|
||||
import { values } from './values.mjs'
|
||||
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
||||
|
||||
export const ns = ['ui-settings']
|
||||
|
||||
const UiSetting = ({ name, control, ...rest }) => (
|
||||
<MenuItem
|
||||
{...rest}
|
||||
name={name}
|
||||
allowToggle={!['control', 'view'].includes(name) && control > 3}
|
||||
control={control}
|
||||
docs={
|
||||
<DynamicMdx
|
||||
language={rest.language}
|
||||
slug={
|
||||
name === 'control'
|
||||
? 'docs/site/account/control'
|
||||
: `docs/site/draft/ui-settings/${name.toLowerCase()}`
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
export const UiSettings = ({ update, ui, control, language, view, setView }) => {
|
||||
const settingsConfig = loadSettingsConfig()
|
||||
|
||||
return (
|
||||
<WorkbenchMenu
|
||||
{...{
|
||||
config: settingsConfig,
|
||||
control,
|
||||
currentValues: ui,
|
||||
Icon: DesktopIcon,
|
||||
inputs,
|
||||
Item: UiSetting,
|
||||
values,
|
||||
language,
|
||||
name: 'uiSettings',
|
||||
ns,
|
||||
updateFunc: update.ui,
|
||||
passProps: { view, setView },
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { ListInput } from '../shared/inputs'
|
||||
import { useControlState } from 'shared/components/account/control.mjs'
|
||||
|
||||
export const ControlSettingInput = (props) => {
|
||||
const { selection, update } = useControlState()
|
||||
|
||||
props.config.dflt = selection
|
||||
return (
|
||||
<ListInput
|
||||
{...props}
|
||||
updateFunc={(path, newVal) => update(newVal)}
|
||||
current={selection}
|
||||
compact={selection < 2}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const inputs = {
|
||||
control: ControlSettingInput,
|
||||
kiosk: ListInput,
|
||||
renderer: ListInput,
|
||||
}
|
|
@ -1,17 +1,19 @@
|
|||
import { Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
import { ClearIcon, FilterIcon, SearchIcon } from 'shared/components/icons.mjs'
|
||||
import { Ul, Li, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu/index.mjs'
|
||||
import { XrayPath } from './path.mjs'
|
||||
import { XrayPoint } from './point.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
//import { Chevron } from 'shared/components/navigation/primary.mjs'
|
||||
//import { ClearIcon, FilterIcon, SearchIcon } from 'shared/components/icons.mjs'
|
||||
//import { XrayPath } from './path.mjs'
|
||||
//import { XrayPoint } from './point.mjs'
|
||||
//import { useTranslation } from 'next-i18next'
|
||||
|
||||
const types = {
|
||||
paths: XrayPath,
|
||||
points: XrayPoint,
|
||||
}
|
||||
//const types = {
|
||||
// paths: XrayPath,
|
||||
// points: XrayPoint,
|
||||
//}
|
||||
|
||||
export const XrayList = (props) => {
|
||||
const { t } = useTranslation(['app', 'parts'])
|
||||
export const XrayList = () => {
|
||||
//const { t } = useTranslation(['app', 'parts'])
|
||||
return null
|
||||
// FIXME
|
||||
/*
|
||||
|
||||
const title = t(`parts:${props.partName}`) + ` (${props.partName})`
|
||||
|
||||
|
@ -159,4 +161,5 @@ export const XrayList = (props) => {
|
|||
</Details>
|
||||
</Li>
|
||||
)
|
||||
*/
|
||||
}
|
43
sites/shared/components/workbench/menus/ui-settings/nl.yaml
Normal file
43
sites/shared/components/workbench/menus/ui-settings/nl.yaml
Normal file
|
@ -0,0 +1,43 @@
|
|||
uiSettings.t: UI-voorkeuren
|
||||
uiSettings.d: Deze voorkeuren bepalen de UI (User Interface) aspecten van onze online omgeving voor het maken van patronen.
|
||||
renderer.t: Render-engine
|
||||
renderer.d: Bepaalt hoe het patroon wordt weergegeven (getekend) op het scherm
|
||||
renderWithReact.t: Renderen met de React-componenten van FreeSewing
|
||||
renderWithReact.d: Renderen als SVG via onze React-componenten. Maakt interactiviteit mogelijk en is geoptimaliseerd voor het scherm. Gebruik deze als je niet zeker weet wat je moet kiezen.
|
||||
renderWithCore.t: Renderen met de Core-bibliotheek van Freesewing
|
||||
renderWithCore.d: Render rechtstreeks naar SVG vanuit Core. Laat geen interactiviteit toe en is geoptimaliseerd voor afdrukken. Gebruik dit als je wilt weten hoe het eruit zal zien als het geëxporteerd wordt.
|
||||
control.t: Gebruikerservaring
|
||||
control.d: Welke gebruikerservaring heeft jouw voorkeur? Houd er rekening mee dat dit een accountinstelling is, dus het heeft invloed op de hele website.
|
||||
inspect.t: Inspecteer
|
||||
inspect.d: Als je dit inschakelt, kun je dieper in het patroon duiken en informatie opvragen over de verschillende onderdelen, paden en punten.
|
||||
inspectNo.t: De inspecteur uitschakelen
|
||||
inspectNo.d: Dit is de standaardinstelling, de patrooninspecteur is uitgeschakeld en het patroon wordt zoals gebruikelijk weergegeven.
|
||||
inspectYes.t: De inspecteur inschakelen
|
||||
inspectYes.d: Met de pattern inspector ingeschakeld en de React rendering engine geselecteerd, voegen we interactiviteit toe aan het patroon zodat je de verschillende elementen waaruit het patroon bestaat kunt inspecteren.
|
||||
no: No
|
||||
yes: Yes
|
||||
draft: Patroontekening
|
||||
test: Test
|
||||
print: Print lay-out
|
||||
cut: Lay-out snijden
|
||||
save: Opslaan
|
||||
export: Exporteren
|
||||
edit: Bewerk
|
||||
draft.t: Teken je patroon
|
||||
draft.d: Lanceert de FreeSewing patroonbewerker, waar je je patroon naar hartenlust kunt aanpassen
|
||||
test.t: Test je patroon
|
||||
test.d: Kijk hoe je patroon zich aanpast aan veranderingen in opties of afmetingen
|
||||
print.t: Lay-out afdrukken
|
||||
print.d: Hiermee kun je je patroondelen ordenen zodat je je patroon op zo min mogelijk pagina's kunt afdrukken
|
||||
cut.t: Lay-out snijden
|
||||
cut.d: Hiermee kun je je patroondelen ordenen zodat je precies kunt bepalen hoeveel stof je nodig hebt om het te maken.
|
||||
save.t: Je patroon opslaan
|
||||
save.d: Sla het huidige patroon op in je FreeSewing account
|
||||
export.t: Je patroon exporteren
|
||||
export.d: Hiermee kun je dit patroon exporteren naar verschillende formaten
|
||||
logs.t: Patroon logboeken
|
||||
logs.d: Hiermee kun je door de patroonlogboeken bladeren om te zien wat er precies is gebeurd tijdens het maken van dit patroon
|
||||
edit.t: De patrooninstellingen met de hand bewerken
|
||||
edit.d: Hiermee kun je de patrooninstellingen met de hand bewerken, zodat je volledige controle hebt over hoe je patroon wordt opgesteld.
|
||||
view: Bekijk
|
||||
view.d: Dit zijn de verschillende weergaven waaruit je kunt kiezen. Inclusief de weergaven die beschikbaar zijn via de navigatiebalk of het menu, en enkele extra weergaven
|
43
sites/shared/components/workbench/menus/ui-settings/uk.yaml
Normal file
43
sites/shared/components/workbench/menus/ui-settings/uk.yaml
Normal file
|
@ -0,0 +1,43 @@
|
|||
uiSettings.t: Налаштування інтерфейсу
|
||||
uiSettings.d: Ці налаштування керують аспектами інтерфейсу користувача (UI) нашого онлайн-середовища для створення викрійок.
|
||||
renderer.t: Рушій рендерингу
|
||||
renderer.d: Керує відтворення викрійки на екрані
|
||||
renderWithReact.t: Рендер за допомогою React-компонентів FreeSewing
|
||||
renderWithReact.d: Відтворення у форматі SVG за допомогою наших React-компонентів. Дозволяє інтерактивність та оптимізований для екрану. Використовуйте його, якщо ви не впевнені, що вибрати.
|
||||
renderWithCore.t: Рендер за допомогою бібліотеки Freesewing Core
|
||||
renderWithCore.d: Рендер безпосередньо в SVG з Core. Не містить інтерактивності та оптимізований для друку. Використовуйте його, якщо хочете знати, як виглядатиме зображення після експорту.
|
||||
control.t: Користувацький досвід
|
||||
control.d: Якому користувацькому досвіду ви надаєте перевагу? Зверніть увагу, що це налаштування облікового запису, тому воно вплине на весь веб-сайт.
|
||||
inspect.t: Оглянути
|
||||
inspect.d: Увімкнувши цю опцію, ви зможете заглибитися в деталь і отримати інформацію про різні її частини, шляхи і точки.
|
||||
inspectNo.t: Відключити інспектора
|
||||
inspectNo.d: За замовчуванням інспектор шаблонів вимкнено, і шаблон відображається у звичайному режимі.
|
||||
inspectYes.t: Увімкніть інспектора
|
||||
inspectYes.d: Увімкнувши інспектор шаблонів і вибравши рушій рендерингу React, ми додамо до шаблону інтерактивність, щоб ви могли оглядати різні елементи, з яких складається шаблон.
|
||||
no: No
|
||||
yes: Yes
|
||||
draft: Чернетка
|
||||
test: Тест
|
||||
print: Макет для друку
|
||||
cut: Макет розкрою
|
||||
save: Зберегти
|
||||
export: Експортувати
|
||||
edit: Редагувати
|
||||
draft.t: Створіть Вашу викрійку
|
||||
draft.d: Запускає флагманський редактор викрійок FreeSewing, де ви можете налаштувати свою викрійку за власним бажанням
|
||||
test.t: Протестувати Вашу викрійку
|
||||
test.d: Подивіться, як ваш лекало адаптується до змін у параметрах або вимірах
|
||||
print.t: Макет для друку
|
||||
print.d: Дозволяє розташувати частини викрійки так, щоб надрукувати її на якомога меншій кількості сторінок
|
||||
cut.t: Схема розкрою
|
||||
cut.d: Дозволяє розташувати частини викрійки так, щоб точно визначити, скільки тканини потрібно для її виготовлення.
|
||||
save.t: Збережіть свій шаблон
|
||||
save.d: Збережіть поточну викрійку у своєму обліковому записі FreeSewing
|
||||
export.t: Експортуйте свій шаблон
|
||||
export.d: Дозволяє експортувати цей шаблон у різні формати
|
||||
logs.t: Журнали шаблонів
|
||||
logs.d: Дозволяє переглянути журнали шаблонів, щоб побачити, що саме відбувалося під час створення цього шаблону
|
||||
edit.t: Відредагуйте налаштування шаблону вручну
|
||||
edit.d: Це дає змогу вручну редагувати налаштування шаблону, що дає вам повний контроль над тим, як буде створено ваш візерунок
|
||||
view: Вигляд
|
||||
view.d: Це різні подання, з яких ви можете вибирати. Включає ті подання, які доступні через панель навігації або меню, а також деякі додаткові
|
|
@ -0,0 +1,9 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { Difficulty } from 'shared/components/designs/difficulty.mjs'
|
||||
import { ListValue } from '../shared/values.mjs'
|
||||
|
||||
export const values = {
|
||||
control: ({ control }) => <Difficulty score={control} color="primary" />,
|
||||
kiosk: ListValue,
|
||||
renderer: ListValue,
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue