@things-factory/figure-ui 10.1.73 → 10.1.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/v3-editor.ts +105 -10
- package/client/modeller/v3-refusals.ts +62 -13
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +14 -0
- package/dist-client/modeller/v3-editor.js +107 -9
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-product-session.d.ts +31 -0
- package/dist-client/modeller/v3-refusals.d.ts +37 -5
- package/dist-client/modeller/v3-refusals.js +59 -16
- package/dist-client/modeller/v3-refusals.js.map +1 -1
- package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/browser/modeller-page.js +14 -0
- package/test/browser/modeller.check.mjs +40 -2
- package/test/v3-parts-wiring.test.ts +6 -2
- package/test/v3-recipes.test.ts +1 -1
- package/test/v3-refusals.test.ts +35 -5
- package/translations/en.json +19 -5
- package/translations/ja.json +19 -5
- package/translations/ko.json +19 -5
- package/translations/ms.json +19 -5
- package/translations/zh.json +19 -5
|
@@ -7,7 +7,7 @@ import { live } from 'lit/directives/live.js'
|
|
|
7
7
|
import { PART_LIMIT, parseV3Recipe, resolveV3Recipe, v3PartCountOf, v3RecipeIdFor } from '@hatiolab/figure-model'
|
|
8
8
|
import type { RecipeTemplate } from './v3-recipes.js'
|
|
9
9
|
import './figure-v3-recipe-catalog.js'
|
|
10
|
-
import type { V3Asset, V3AuthoringAction, V3InputChange, V3PartEdit, V3PrimitiveCreation, V3Recipe, V3RecipeValue } from '@hatiolab/figure-model'
|
|
10
|
+
import type { V3Asset, V3AuthoringAction, V3InputChange, V3PartEdit, V3PrimitiveCreation, V3ProposalStep, V3Recipe, V3RecipeValue } from '@hatiolab/figure-model'
|
|
11
11
|
import type { V3ProductSession } from './v3-product-session.js'
|
|
12
12
|
import { captureV3Thumbnail } from './v3-thumbnail.js'
|
|
13
13
|
import { drawableV3Parts } from './v3-renderer.js'
|
|
@@ -31,7 +31,7 @@ import type { V3PaletteKind, V3PartRow } from './figure-v3-parts.js'
|
|
|
31
31
|
import { paletteRecipe } from './v3-palette.js'
|
|
32
32
|
import type { FigureV3Canvas } from './v3-canvas.js'
|
|
33
33
|
import { nudgeOf } from './v3-stage-model.js'
|
|
34
|
-
import { inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js'
|
|
34
|
+
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './v3-refusals.js'
|
|
35
35
|
import type { V3RenderPart } from './v3-renderer.js'
|
|
36
36
|
import type { V3BudgetLine } from './v3-maturity.js'
|
|
37
37
|
import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js'
|
|
@@ -269,6 +269,8 @@ export class FigureV3Editor extends LitElement {
|
|
|
269
269
|
/** Reads a template's guide; given by the page. */
|
|
270
270
|
@property({ attribute: false }) guideOf?: (name: string) => Promise<string>
|
|
271
271
|
@property({ attribute: false }) thumbnailOf?: (name: string) => string
|
|
272
|
+
/** The 「보드에서 바꿀 수 있음」 form being filled for a row, before it is published (ADR-0096). */
|
|
273
|
+
@state() private publishing?: { part: string; label: string; min: number; max: number }
|
|
272
274
|
@state() private catalogOpen = false
|
|
273
275
|
/** Why the last recipe was refused, in the kernel's words. */
|
|
274
276
|
@state() private recipeError = ''
|
|
@@ -400,14 +402,23 @@ export class FigureV3Editor extends LitElement {
|
|
|
400
402
|
this.renderRoot.querySelector<FigureV3Canvas>('figure-v3-canvas')?.gizmo(mode)
|
|
401
403
|
this.gizmoMode = this.gizmoMode === mode ? undefined : mode
|
|
402
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
|
|
407
|
+
* (the network, the server) as it came.
|
|
408
|
+
*/
|
|
409
|
+
private said(error: unknown): string {
|
|
410
|
+
return (error as { code?: string })?.code ? editRefusalOf(error, (key, params) => i18next.t(key, params)) : error instanceof Error ? error.message : String(error)
|
|
411
|
+
}
|
|
403
412
|
private run(action: () => unknown) {
|
|
404
413
|
try {
|
|
405
414
|
action()
|
|
406
415
|
this.error = ''
|
|
416
|
+
/* The next step stands on its own: what the last removal let go of is no longer the news. */
|
|
417
|
+
this.notice = ''
|
|
407
418
|
this.changed()
|
|
408
419
|
} catch (error) {
|
|
409
420
|
const say = (key: string, params: Record<string, string>) => i18next.t(key, params)
|
|
410
|
-
this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ??
|
|
421
|
+
this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? this.said(error)
|
|
411
422
|
} finally {
|
|
412
423
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
413
424
|
this.live = NOTHING_LIVE
|
|
@@ -473,7 +484,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
473
484
|
await promise
|
|
474
485
|
this.error = ''
|
|
475
486
|
} catch (error) {
|
|
476
|
-
this.error =
|
|
487
|
+
this.error = this.said(error)
|
|
477
488
|
} finally {
|
|
478
489
|
this.changed()
|
|
479
490
|
}
|
|
@@ -506,13 +517,15 @@ export class FigureV3Editor extends LitElement {
|
|
|
506
517
|
private removePart(id: string, refitRoom = false) {
|
|
507
518
|
this.retry = undefined
|
|
508
519
|
try {
|
|
509
|
-
this.session!.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) })
|
|
520
|
+
const view = this.session!.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) }) as { removal?: Parameters<typeof removalNoticeOf>[0] }
|
|
510
521
|
this.error = ''
|
|
522
|
+
/* What else the removal let go of, named (chief architect, 2026-09-27); undo brings it back. */
|
|
523
|
+
this.notice = removalNoticeOf(view.removal, (key, params) => i18next.t(key, params)) ?? ''
|
|
511
524
|
if (this.selected === id) this.selected = ''
|
|
512
525
|
this.changed()
|
|
513
526
|
} catch (error) {
|
|
514
527
|
/* The refusal a person meets most: another part stands on this one or follows its size. Said in their words. */
|
|
515
|
-
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ??
|
|
528
|
+
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error)
|
|
516
529
|
/* In the declared room only: the way on is to delete it and fit the room to the parts left, in one step. */
|
|
517
530
|
if (!refitRoom && inDeclaredRoom(error)) this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) }
|
|
518
531
|
}
|
|
@@ -544,7 +557,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
544
557
|
this.error = ''
|
|
545
558
|
this.changed()
|
|
546
559
|
} catch (error) {
|
|
547
|
-
this.recipeError = error
|
|
560
|
+
this.recipeError = recipeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? this.said(error)
|
|
548
561
|
}
|
|
549
562
|
}
|
|
550
563
|
/** The design values of the asset being edited. */
|
|
@@ -628,7 +641,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
628
641
|
? i18next.t('figure.text.gizmo-joint-places', { part: partLabelOf(id) })
|
|
629
642
|
: /is fastened to/.test(message)
|
|
630
643
|
? i18next.t('figure.text.gizmo-fastening-places', { part: partLabelOf(id) })
|
|
631
|
-
:
|
|
644
|
+
: this.said(error)
|
|
632
645
|
this.requestUpdate()
|
|
633
646
|
}
|
|
634
647
|
}
|
|
@@ -801,7 +814,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
801
814
|
? i18next.t('figure.text.fasten-would-loop')
|
|
802
815
|
: sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ??
|
|
803
816
|
placementRefusalOf(error, (key, params) => i18next.t(key, params)) ??
|
|
804
|
-
|
|
817
|
+
this.said(error)
|
|
805
818
|
}
|
|
806
819
|
}
|
|
807
820
|
private changeField(field: { id: string; unit: V3InputChange['unit']; role?: string }, event: Event) {
|
|
@@ -1040,6 +1053,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
1040
1053
|
})
|
|
1041
1054
|
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
1042
1055
|
</p>
|
|
1056
|
+
${this.renderPublicPitch(asset, pitch ?? 0, unavailable)}
|
|
1043
1057
|
${asset.joints?.some(j => j.body1.part === this.selected && j.repeat)
|
|
1044
1058
|
? html`<button act ?disabled=${unavailable} @click=${() => this.run(() => this.session!.command({ kind: 'remove-repeat', part: this.selected }))}>
|
|
1045
1059
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-repeat')}
|
|
@@ -1047,6 +1061,80 @@ export class FigureV3Editor extends LitElement {
|
|
|
1047
1061
|
: nothing}
|
|
1048
1062
|
</section>`
|
|
1049
1063
|
}
|
|
1064
|
+
/** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
|
|
1065
|
+
private pitchInputOf(asset: V3Asset, part: string): string | undefined {
|
|
1066
|
+
const pitch = asset.joints?.find(j => j.body1.part === part && j.repeat)?.repeat?.pitch as { from?: { of?: string; input?: string } } | number | undefined
|
|
1067
|
+
return typeof pitch === 'object' && pitch?.from?.of === 'input' ? pitch.from.input : undefined
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* 「보드에서 바꿀 수 있음」 (ADR-0096): the row's pitch made a design input the figure publishes -- a name on the board and
|
|
1071
|
+
* a range the board's value is held to -- in one step; changed or withdrawn the same way. Only a row with a record.
|
|
1072
|
+
*/
|
|
1073
|
+
private renderPublicPitch(asset: V3Asset, pitch: number, unavailable: boolean) {
|
|
1074
|
+
const part = this.selected
|
|
1075
|
+
if (!asset.joints?.some(j => j.body1.part === part && j.repeat)) return nothing
|
|
1076
|
+
const from = this.pitchInputOf(asset, part)
|
|
1077
|
+
const listed = from ? asset.publicInputs?.find(p => p.input === from) : undefined
|
|
1078
|
+
const input = from ? asset.document.model.inputs.find(i => i.id === from) : undefined
|
|
1079
|
+
const form =
|
|
1080
|
+
this.publishing?.part === part
|
|
1081
|
+
? this.publishing
|
|
1082
|
+
: listed && input
|
|
1083
|
+
? { part, label: listed.label, min: input.min, max: input.max }
|
|
1084
|
+
: undefined
|
|
1085
|
+
const open = !!form
|
|
1086
|
+
const edit = (key: 'label' | 'min' | 'max') => (e: Event) => {
|
|
1087
|
+
const target = e.target as HTMLInputElement
|
|
1088
|
+
const base = form ?? { part, label: '', min: 0, max: 0 }
|
|
1089
|
+
this.publishing = { ...base, [key]: key === 'label' ? target.value : target.valueAsNumber }
|
|
1090
|
+
}
|
|
1091
|
+
const steps = (): V3ProposalStep[] => {
|
|
1092
|
+
const f = form!
|
|
1093
|
+
const id = from ?? `${part}.pitch`
|
|
1094
|
+
if (listed) return [{ kind: 'set-input-range', input: id, min: f.min, max: f.max }, { kind: 'publish-input', input: id, label: f.label }] as V3ProposalStep[]
|
|
1095
|
+
const exists = asset.document.model.inputs.some(i => i.id === id)
|
|
1096
|
+
return [
|
|
1097
|
+
...(exists ? [{ kind: 'set-input-range', input: id, min: f.min, max: f.max }] : [{ kind: 'add-design-input', id, unit: 'mm', min: f.min, max: f.max, value: pitch }]),
|
|
1098
|
+
...(from ? [] : [{ kind: 'set-repeat', part, pitch: { from: { of: 'input', input: id } } }]),
|
|
1099
|
+
{ kind: 'publish-input', input: id, label: f.label }
|
|
1100
|
+
] as V3ProposalStep[]
|
|
1101
|
+
}
|
|
1102
|
+
const run = (list: V3ProposalStep[]) => {
|
|
1103
|
+
try {
|
|
1104
|
+
this.session!.applyRecipe(list)
|
|
1105
|
+
this.publishing = undefined
|
|
1106
|
+
this.error = ''
|
|
1107
|
+
this.changed()
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
this.error = this.said(error)
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return html`<div public-pitch>
|
|
1113
|
+
<label toggle>
|
|
1114
|
+
<input
|
|
1115
|
+
type="checkbox"
|
|
1116
|
+
aria-label="figure.label.board-sets"
|
|
1117
|
+
.checked=${open}
|
|
1118
|
+
?disabled=${unavailable}
|
|
1119
|
+
@change=${(e: Event) =>
|
|
1120
|
+
(e.target as HTMLInputElement).checked
|
|
1121
|
+
? (this.publishing = { part, label: i18next.t('figure.label.repeat-gap'), min: Math.max(1, Math.round(pitch / 2)), max: Math.round(pitch * 2) })
|
|
1122
|
+
: listed
|
|
1123
|
+
? run([{ kind: 'unpublish-input', input: from }, { kind: 'set-repeat', part, pitch }] as V3ProposalStep[])
|
|
1124
|
+
: (this.publishing = undefined)}
|
|
1125
|
+
/>
|
|
1126
|
+
${i18next.t('figure.label.board-sets')}
|
|
1127
|
+
</label>
|
|
1128
|
+
${open
|
|
1129
|
+
? html`<div inline-field><label>${i18next.t('figure.label.board-name')}</label><input aria-label="figure.label.board-name" .value=${form!.label} @change=${edit('label')} /></div>
|
|
1130
|
+
<div inline-field><label>${i18next.t('figure.label.least')} <span unit>mm</span></label><input type="number" step="any" aria-label="figure.label.least" .value=${String(form!.min)} @change=${edit('min')} /></div>
|
|
1131
|
+
<div inline-field><label>${i18next.t('figure.label.most')} <span unit>mm</span></label><input type="number" step="any" aria-label="figure.label.most" .value=${String(form!.max)} @change=${edit('max')} /></div>
|
|
1132
|
+
<button act publish-pitch ?disabled=${unavailable || !form!.label.trim()} @click=${() => run(steps())}>
|
|
1133
|
+
<md-icon>${listed ? 'check' : 'ios_share'}</md-icon>${i18next.t(listed ? 'figure.button.apply' : 'figure.button.publish-input')}
|
|
1134
|
+
</button>`
|
|
1135
|
+
: nothing}
|
|
1136
|
+
</div>`
|
|
1137
|
+
}
|
|
1050
1138
|
/**
|
|
1051
1139
|
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
1052
1140
|
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
@@ -1056,6 +1144,13 @@ export class FigureV3Editor extends LitElement {
|
|
|
1056
1144
|
const input = event.target as HTMLInputElement
|
|
1057
1145
|
const pitch = input.valueAsNumber
|
|
1058
1146
|
if (!Number.isFinite(pitch) || pitch === field.value) return
|
|
1147
|
+
/* Read from a design input the board may set: the input's value changes, the row keeps reading it (ADR-0096). */
|
|
1148
|
+
const from = this.pitchInputOf(JSON.parse(this.session!.source()) as V3Asset, this.selected)
|
|
1149
|
+
if (from) {
|
|
1150
|
+
this.run(() => this.session!.apply([{ kind: 'set-design', input: from, unit: 'mm', value: pitch }]))
|
|
1151
|
+
input.value = String(this.session!.read().fields.find(i => i.id === from)?.value ?? field.value)
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1059
1154
|
try {
|
|
1060
1155
|
this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })
|
|
1061
1156
|
this.error = ''
|
|
@@ -1068,7 +1163,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
1068
1163
|
this.error =
|
|
1069
1164
|
code === 'INPUT_RANGE' && found
|
|
1070
1165
|
? i18next.t('figure.text.repeat-pitch-too-small', { length: Number(found[1]).toLocaleString(), min: found[2], limit: found[3] })
|
|
1071
|
-
:
|
|
1166
|
+
: this.said(error)
|
|
1072
1167
|
}
|
|
1073
1168
|
}
|
|
1074
1169
|
/** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
|
|
@@ -39,11 +39,6 @@ export function narrowingRetryOf<A extends { kind: string }>(action: A, error: u
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
* Why a part cannot be deleted, in the author's words -- figure-model names four reasons (PART_REFERENCED): parts hang
|
|
44
|
-
* from it, the declared space holds it, another part follows its size or place, or a joint moves with its joint. One
|
|
45
|
-
* sentence for all of them told the author to undo a link that was not there (the architect's workbench, 2026-09-25).
|
|
46
|
-
*/
|
|
47
42
|
/**
|
|
48
43
|
* A placement refused because a part is fastened to the mounting plane by a face the new plane does not take (a bottom
|
|
49
44
|
* on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.
|
|
@@ -61,20 +56,74 @@ export function inDeclaredRoom(error: unknown): boolean {
|
|
|
61
56
|
return (error as { code?: string })?.code === 'PART_REFERENCED' && /is in the space the figure declares/.test(error instanceof Error ? error.message : String(error))
|
|
62
57
|
}
|
|
63
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Why a part cannot be deleted, in the author's words. Since the chief architect's ruling of 2026-09-27 a part that
|
|
61
|
+
* holds others, leads a motion or is followed goes, and what depended on it is let go of (`removalNoticeOf`); what is
|
|
62
|
+
* left is the declared space (with a way on: fit it again) and a row -- one laid along the part, or hanging in its row.
|
|
63
|
+
*/
|
|
64
64
|
export function partRefusalOf(error: unknown, part: string, say: Say): string | null {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const code = (error as { code?: string })?.code
|
|
66
|
+
if (code !== 'PART_REFERENCED' && code !== 'PART_ROW') return null
|
|
67
|
+
/* figure-model's sentence after its code and path: 「PART_ROW: bed: roller is a row laid along bed; …」. */
|
|
67
68
|
const message = (error instanceof Error ? error.message : String(error)).replace(/^[A-Z_]+: [^:]+: /, '')
|
|
68
|
-
const holds = / holds (.+?); take (?:them|it) off first/.exec(message)
|
|
69
|
-
if (holds) return say('figure.text.part-holds-others', { part, others: holds[1]! })
|
|
70
69
|
if (/is in the space the figure declares/.test(message)) return say('figure.text.part-in-declared-space', { part })
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
70
|
+
const along = /^(.+?) (?:is a row|are rows) laid along /.exec(message)
|
|
71
|
+
if (along) return say('figure.text.part-row-along', { part, rows: along[1]! })
|
|
72
|
+
const carried = /^(.+?) hangs? in the row /.exec(message)
|
|
73
|
+
if (carried) return say('figure.text.part-row-carries', { part, others: carried[1]! })
|
|
75
74
|
return say('figure.text.part-is-referenced', { part })
|
|
76
75
|
}
|
|
77
76
|
|
|
77
|
+
/** What removing a part let go of (figure-model's `RemovalNotice`), said in one line, or nothing when it let go of nothing. */
|
|
78
|
+
export function removalNoticeOf(
|
|
79
|
+
notice:
|
|
80
|
+
| {
|
|
81
|
+
released: Array<{ part: string; control?: string }>
|
|
82
|
+
fixed: Array<{ part: string }>
|
|
83
|
+
controls: Array<{ part: string; control: string; min: number; max: number }>
|
|
84
|
+
mountingFace?: boolean
|
|
85
|
+
bindings?: Array<{ id: string; label?: string }>
|
|
86
|
+
}
|
|
87
|
+
| undefined,
|
|
88
|
+
say: Say
|
|
89
|
+
): string | null {
|
|
90
|
+
if (!notice) return null
|
|
91
|
+
const number = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 3 })
|
|
92
|
+
const lines: string[] = []
|
|
93
|
+
const still = notice.released.filter(r => !r.control).map(r => r.part)
|
|
94
|
+
if (still.length) lines.push(say('figure.text.removal-released', { parts: still.join(', ') }))
|
|
95
|
+
for (const r of notice.released.filter(r => r.control)) lines.push(say('figure.text.removal-released-motion', { part: r.part, control: r.control! }))
|
|
96
|
+
for (const c of notice.controls) lines.push(say('figure.text.removal-own-control', { part: c.part, control: c.control, min: number(c.min), max: number(c.max) }))
|
|
97
|
+
const fixed = [...new Set(notice.fixed.map(f => f.part))]
|
|
98
|
+
if (fixed.length) lines.push(say('figure.text.removal-fixed', { parts: fixed.join(', ') }))
|
|
99
|
+
if (notice.bindings?.length) lines.push(say('figure.text.removal-bindings', { places: notice.bindings.map(b => b.label ?? b.id).join(', ') }))
|
|
100
|
+
if (notice.mountingFace) lines.push(say('figure.text.removal-mounting-face', {}))
|
|
101
|
+
return lines.length ? lines.join(' ') : null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A recipe refused at one of its steps: which step, and why -- the particular sentence when there is one (a size
|
|
106
|
+
* range, a value's range, a placement), otherwise the kernel's code. The kernel says 「steps[2]: attach: …」.
|
|
107
|
+
*/
|
|
108
|
+
export function recipeRefusalOf(error: unknown, say: Say): string | null {
|
|
109
|
+
const step = /^steps\[(\d+)\]$/.exec((error as { path?: string })?.path ?? '')
|
|
110
|
+
if (!step) return null
|
|
111
|
+
const why = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? placementRefusalOf(error, say)
|
|
112
|
+
return say('figure.text.recipe-step-refused', { step: String(Number(step[1]) + 1), why: why ?? '', code: (error as { code?: string })?.code ?? '' })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A refusal no sentence above knows, said as a refusal with its code -- never the kernel's English on the screen (the
|
|
117
|
+
* duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」, 2026-09-27). The figure's node limit
|
|
118
|
+
* is said with its number.
|
|
119
|
+
*/
|
|
120
|
+
export function editRefusalOf(error: unknown, say: Say): string {
|
|
121
|
+
const code = (error as { code?: string })?.code
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
123
|
+
if (code === 'DOCUMENT_LIMIT') return say('figure.text.edit-refused-node-limit', { max: /maximum (\d+) nodes/.exec(message)?.[1] ?? '' })
|
|
124
|
+
return say('figure.text.edit-refused', { code: code ?? '' })
|
|
125
|
+
}
|
|
126
|
+
|
|
78
127
|
/**
|
|
79
128
|
* A value outside the range its input declares, with the range and the value given -- the kernel says
|
|
80
129
|
* 「size.x: 6021100 outside [300,1200]」. The figure's own size is named by its axis; any other input by its range only.
|
|
@@ -23,6 +23,8 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
23
23
|
/** Reads a template's guide; given by the page. */
|
|
24
24
|
guideOf?: (name: string) => Promise<string>;
|
|
25
25
|
thumbnailOf?: (name: string) => string;
|
|
26
|
+
/** The 「보드에서 바꿀 수 있음」 form being filled for a row, before it is published (ADR-0096). */
|
|
27
|
+
private publishing?;
|
|
26
28
|
private catalogOpen;
|
|
27
29
|
/** Why the last recipe was refused, in the kernel's words. */
|
|
28
30
|
private recipeError;
|
|
@@ -77,6 +79,11 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
77
79
|
protected willUpdate(_changed: PropertyValues): void;
|
|
78
80
|
/** The scene toggles: the mode already shown takes the gizmo away. */
|
|
79
81
|
private useGizmo;
|
|
82
|
+
/**
|
|
83
|
+
* A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
|
|
84
|
+
* (the network, the server) as it came.
|
|
85
|
+
*/
|
|
86
|
+
private said;
|
|
80
87
|
private run;
|
|
81
88
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
82
89
|
private drawnParts;
|
|
@@ -168,6 +175,13 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
168
175
|
* time); a row of a fixed count says its pitch cannot be set yet (figure-model refuses it the same way).
|
|
169
176
|
*/
|
|
170
177
|
private renderRepeat;
|
|
178
|
+
/** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
|
|
179
|
+
private pitchInputOf;
|
|
180
|
+
/**
|
|
181
|
+
* 「보드에서 바꿀 수 있음」 (ADR-0096): the row's pitch made a design input the figure publishes -- a name on the board and
|
|
182
|
+
* a range the board's value is held to -- in one step; changed or withdrawn the same way. Only a row with a record.
|
|
183
|
+
*/
|
|
184
|
+
private renderPublicPitch;
|
|
171
185
|
/**
|
|
172
186
|
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
173
187
|
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
@@ -27,7 +27,7 @@ import { ScrollbarStyles } from '@operato/styles';
|
|
|
27
27
|
import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
|
|
28
28
|
import { paletteRecipe } from './v3-palette.js';
|
|
29
29
|
import { nudgeOf } from './v3-stage-model.js';
|
|
30
|
-
import { inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js';
|
|
30
|
+
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './v3-refusals.js';
|
|
31
31
|
import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js';
|
|
32
32
|
import { jointFanOf } from './v3-joint-fan.js';
|
|
33
33
|
import { jointDragOf } from './v3-joint-drag.js';
|
|
@@ -376,15 +376,24 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
376
376
|
this.renderRoot.querySelector('figure-v3-canvas')?.gizmo(mode);
|
|
377
377
|
this.gizmoMode = this.gizmoMode === mode ? undefined : mode;
|
|
378
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* A refusal no particular sentence knows: figure-model's code said as a refusal, never its English; any other error
|
|
381
|
+
* (the network, the server) as it came.
|
|
382
|
+
*/
|
|
383
|
+
said(error) {
|
|
384
|
+
return error?.code ? editRefusalOf(error, (key, params) => i18next.t(key, params)) : error instanceof Error ? error.message : String(error);
|
|
385
|
+
}
|
|
379
386
|
run(action) {
|
|
380
387
|
try {
|
|
381
388
|
action();
|
|
382
389
|
this.error = '';
|
|
390
|
+
/* The next step stands on its own: what the last removal let go of is no longer the news. */
|
|
391
|
+
this.notice = '';
|
|
383
392
|
this.changed();
|
|
384
393
|
}
|
|
385
394
|
catch (error) {
|
|
386
395
|
const say = (key, params) => i18next.t(key, params);
|
|
387
|
-
this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ??
|
|
396
|
+
this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? this.said(error);
|
|
388
397
|
}
|
|
389
398
|
finally {
|
|
390
399
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
@@ -456,7 +465,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
456
465
|
this.error = '';
|
|
457
466
|
}
|
|
458
467
|
catch (error) {
|
|
459
|
-
this.error =
|
|
468
|
+
this.error = this.said(error);
|
|
460
469
|
}
|
|
461
470
|
finally {
|
|
462
471
|
this.changed();
|
|
@@ -492,15 +501,17 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
492
501
|
removePart(id, refitRoom = false) {
|
|
493
502
|
this.retry = undefined;
|
|
494
503
|
try {
|
|
495
|
-
this.session.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) });
|
|
504
|
+
const view = this.session.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) });
|
|
496
505
|
this.error = '';
|
|
506
|
+
/* What else the removal let go of, named (chief architect, 2026-09-27); undo brings it back. */
|
|
507
|
+
this.notice = removalNoticeOf(view.removal, (key, params) => i18next.t(key, params)) ?? '';
|
|
497
508
|
if (this.selected === id)
|
|
498
509
|
this.selected = '';
|
|
499
510
|
this.changed();
|
|
500
511
|
}
|
|
501
512
|
catch (error) {
|
|
502
513
|
/* The refusal a person meets most: another part stands on this one or follows its size. Said in their words. */
|
|
503
|
-
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ??
|
|
514
|
+
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error);
|
|
504
515
|
/* In the declared room only: the way on is to delete it and fit the room to the parts left, in one step. */
|
|
505
516
|
if (!refitRoom && inDeclaredRoom(error))
|
|
506
517
|
this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) };
|
|
@@ -536,7 +547,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
536
547
|
this.changed();
|
|
537
548
|
}
|
|
538
549
|
catch (error) {
|
|
539
|
-
this.recipeError = error
|
|
550
|
+
this.recipeError = recipeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? this.said(error);
|
|
540
551
|
}
|
|
541
552
|
}
|
|
542
553
|
/** The design values of the asset being edited. */
|
|
@@ -629,7 +640,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
629
640
|
? i18next.t('figure.text.gizmo-joint-places', { part: partLabelOf(id) })
|
|
630
641
|
: /is fastened to/.test(message)
|
|
631
642
|
? i18next.t('figure.text.gizmo-fastening-places', { part: partLabelOf(id) })
|
|
632
|
-
:
|
|
643
|
+
: this.said(error);
|
|
633
644
|
this.requestUpdate();
|
|
634
645
|
}
|
|
635
646
|
}
|
|
@@ -810,7 +821,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
810
821
|
? i18next.t('figure.text.fasten-would-loop')
|
|
811
822
|
: sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ??
|
|
812
823
|
placementRefusalOf(error, (key, params) => i18next.t(key, params)) ??
|
|
813
|
-
|
|
824
|
+
this.said(error);
|
|
814
825
|
}
|
|
815
826
|
}
|
|
816
827
|
changeField(field, event) {
|
|
@@ -1049,6 +1060,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1049
1060
|
})
|
|
1050
1061
|
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
1051
1062
|
</p>
|
|
1063
|
+
${this.renderPublicPitch(asset, pitch ?? 0, unavailable)}
|
|
1052
1064
|
${asset.joints?.some(j => j.body1.part === this.selected && j.repeat)
|
|
1053
1065
|
? html `<button act ?disabled=${unavailable} @click=${() => this.run(() => this.session.command({ kind: 'remove-repeat', part: this.selected }))}>
|
|
1054
1066
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-repeat')}
|
|
@@ -1056,6 +1068,81 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1056
1068
|
: nothing}
|
|
1057
1069
|
</section>`;
|
|
1058
1070
|
}
|
|
1071
|
+
/** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
|
|
1072
|
+
pitchInputOf(asset, part) {
|
|
1073
|
+
const pitch = asset.joints?.find(j => j.body1.part === part && j.repeat)?.repeat?.pitch;
|
|
1074
|
+
return typeof pitch === 'object' && pitch?.from?.of === 'input' ? pitch.from.input : undefined;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* 「보드에서 바꿀 수 있음」 (ADR-0096): the row's pitch made a design input the figure publishes -- a name on the board and
|
|
1078
|
+
* a range the board's value is held to -- in one step; changed or withdrawn the same way. Only a row with a record.
|
|
1079
|
+
*/
|
|
1080
|
+
renderPublicPitch(asset, pitch, unavailable) {
|
|
1081
|
+
const part = this.selected;
|
|
1082
|
+
if (!asset.joints?.some(j => j.body1.part === part && j.repeat))
|
|
1083
|
+
return nothing;
|
|
1084
|
+
const from = this.pitchInputOf(asset, part);
|
|
1085
|
+
const listed = from ? asset.publicInputs?.find(p => p.input === from) : undefined;
|
|
1086
|
+
const input = from ? asset.document.model.inputs.find(i => i.id === from) : undefined;
|
|
1087
|
+
const form = this.publishing?.part === part
|
|
1088
|
+
? this.publishing
|
|
1089
|
+
: listed && input
|
|
1090
|
+
? { part, label: listed.label, min: input.min, max: input.max }
|
|
1091
|
+
: undefined;
|
|
1092
|
+
const open = !!form;
|
|
1093
|
+
const edit = (key) => (e) => {
|
|
1094
|
+
const target = e.target;
|
|
1095
|
+
const base = form ?? { part, label: '', min: 0, max: 0 };
|
|
1096
|
+
this.publishing = { ...base, [key]: key === 'label' ? target.value : target.valueAsNumber };
|
|
1097
|
+
};
|
|
1098
|
+
const steps = () => {
|
|
1099
|
+
const f = form;
|
|
1100
|
+
const id = from ?? `${part}.pitch`;
|
|
1101
|
+
if (listed)
|
|
1102
|
+
return [{ kind: 'set-input-range', input: id, min: f.min, max: f.max }, { kind: 'publish-input', input: id, label: f.label }];
|
|
1103
|
+
const exists = asset.document.model.inputs.some(i => i.id === id);
|
|
1104
|
+
return [
|
|
1105
|
+
...(exists ? [{ kind: 'set-input-range', input: id, min: f.min, max: f.max }] : [{ kind: 'add-design-input', id, unit: 'mm', min: f.min, max: f.max, value: pitch }]),
|
|
1106
|
+
...(from ? [] : [{ kind: 'set-repeat', part, pitch: { from: { of: 'input', input: id } } }]),
|
|
1107
|
+
{ kind: 'publish-input', input: id, label: f.label }
|
|
1108
|
+
];
|
|
1109
|
+
};
|
|
1110
|
+
const run = (list) => {
|
|
1111
|
+
try {
|
|
1112
|
+
this.session.applyRecipe(list);
|
|
1113
|
+
this.publishing = undefined;
|
|
1114
|
+
this.error = '';
|
|
1115
|
+
this.changed();
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
this.error = this.said(error);
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
return html `<div public-pitch>
|
|
1122
|
+
<label toggle>
|
|
1123
|
+
<input
|
|
1124
|
+
type="checkbox"
|
|
1125
|
+
aria-label="figure.label.board-sets"
|
|
1126
|
+
.checked=${open}
|
|
1127
|
+
?disabled=${unavailable}
|
|
1128
|
+
@change=${(e) => e.target.checked
|
|
1129
|
+
? (this.publishing = { part, label: i18next.t('figure.label.repeat-gap'), min: Math.max(1, Math.round(pitch / 2)), max: Math.round(pitch * 2) })
|
|
1130
|
+
: listed
|
|
1131
|
+
? run([{ kind: 'unpublish-input', input: from }, { kind: 'set-repeat', part, pitch }])
|
|
1132
|
+
: (this.publishing = undefined)}
|
|
1133
|
+
/>
|
|
1134
|
+
${i18next.t('figure.label.board-sets')}
|
|
1135
|
+
</label>
|
|
1136
|
+
${open
|
|
1137
|
+
? html `<div inline-field><label>${i18next.t('figure.label.board-name')}</label><input aria-label="figure.label.board-name" .value=${form.label} @change=${edit('label')} /></div>
|
|
1138
|
+
<div inline-field><label>${i18next.t('figure.label.least')} <span unit>mm</span></label><input type="number" step="any" aria-label="figure.label.least" .value=${String(form.min)} @change=${edit('min')} /></div>
|
|
1139
|
+
<div inline-field><label>${i18next.t('figure.label.most')} <span unit>mm</span></label><input type="number" step="any" aria-label="figure.label.most" .value=${String(form.max)} @change=${edit('max')} /></div>
|
|
1140
|
+
<button act publish-pitch ?disabled=${unavailable || !form.label.trim()} @click=${() => run(steps())}>
|
|
1141
|
+
<md-icon>${listed ? 'check' : 'ios_share'}</md-icon>${i18next.t(listed ? 'figure.button.apply' : 'figure.button.publish-input')}
|
|
1142
|
+
</button>`
|
|
1143
|
+
: nothing}
|
|
1144
|
+
</div>`;
|
|
1145
|
+
}
|
|
1059
1146
|
/**
|
|
1060
1147
|
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
1061
1148
|
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
@@ -1066,6 +1153,13 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1066
1153
|
const pitch = input.valueAsNumber;
|
|
1067
1154
|
if (!Number.isFinite(pitch) || pitch === field.value)
|
|
1068
1155
|
return;
|
|
1156
|
+
/* Read from a design input the board may set: the input's value changes, the row keeps reading it (ADR-0096). */
|
|
1157
|
+
const from = this.pitchInputOf(JSON.parse(this.session.source()), this.selected);
|
|
1158
|
+
if (from) {
|
|
1159
|
+
this.run(() => this.session.apply([{ kind: 'set-design', input: from, unit: 'mm', value: pitch }]));
|
|
1160
|
+
input.value = String(this.session.read().fields.find(i => i.id === from)?.value ?? field.value);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1069
1163
|
try {
|
|
1070
1164
|
this.session.command({ kind: 'set-repeat', part: this.selected, pitch });
|
|
1071
1165
|
this.error = '';
|
|
@@ -1079,7 +1173,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1079
1173
|
this.error =
|
|
1080
1174
|
code === 'INPUT_RANGE' && found
|
|
1081
1175
|
? i18next.t('figure.text.repeat-pitch-too-small', { length: Number(found[1]).toLocaleString(), min: found[2], limit: found[3] })
|
|
1082
|
-
:
|
|
1176
|
+
: this.said(error);
|
|
1083
1177
|
}
|
|
1084
1178
|
}
|
|
1085
1179
|
/** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
|
|
@@ -1353,6 +1447,10 @@ __decorate([
|
|
|
1353
1447
|
property({ attribute: false }),
|
|
1354
1448
|
__metadata("design:type", Function)
|
|
1355
1449
|
], FigureV3Editor.prototype, "thumbnailOf", void 0);
|
|
1450
|
+
__decorate([
|
|
1451
|
+
state(),
|
|
1452
|
+
__metadata("design:type", Object)
|
|
1453
|
+
], FigureV3Editor.prototype, "publishing", void 0);
|
|
1356
1454
|
__decorate([
|
|
1357
1455
|
state(),
|
|
1358
1456
|
__metadata("design:type", Object)
|