@things-factory/figure-ui 10.1.73 → 10.1.74
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 +85 -1
- package/dist-client/modeller/v3-editor.d.ts +9 -0
- package/dist-client/modeller/v3-editor.js +87 -0
- package/dist-client/modeller/v3-editor.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 +37 -0
- package/translations/en.json +7 -1
- package/translations/ja.json +7 -1
- package/translations/ko.json +7 -1
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
|
@@ -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'
|
|
@@ -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 = ''
|
|
@@ -1040,6 +1042,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
1040
1042
|
})
|
|
1041
1043
|
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
1042
1044
|
</p>
|
|
1045
|
+
${this.renderPublicPitch(asset, pitch ?? 0, unavailable)}
|
|
1043
1046
|
${asset.joints?.some(j => j.body1.part === this.selected && j.repeat)
|
|
1044
1047
|
? html`<button act ?disabled=${unavailable} @click=${() => this.run(() => this.session!.command({ kind: 'remove-repeat', part: this.selected }))}>
|
|
1045
1048
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-repeat')}
|
|
@@ -1047,6 +1050,80 @@ export class FigureV3Editor extends LitElement {
|
|
|
1047
1050
|
: nothing}
|
|
1048
1051
|
</section>`
|
|
1049
1052
|
}
|
|
1053
|
+
/** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
|
|
1054
|
+
private pitchInputOf(asset: V3Asset, part: string): string | undefined {
|
|
1055
|
+
const pitch = asset.joints?.find(j => j.body1.part === part && j.repeat)?.repeat?.pitch as { from?: { of?: string; input?: string } } | number | undefined
|
|
1056
|
+
return typeof pitch === 'object' && pitch?.from?.of === 'input' ? pitch.from.input : undefined
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* 「보드에서 바꿀 수 있음」 (ADR-0096): the row's pitch made a design input the figure publishes -- a name on the board and
|
|
1060
|
+
* a range the board's value is held to -- in one step; changed or withdrawn the same way. Only a row with a record.
|
|
1061
|
+
*/
|
|
1062
|
+
private renderPublicPitch(asset: V3Asset, pitch: number, unavailable: boolean) {
|
|
1063
|
+
const part = this.selected
|
|
1064
|
+
if (!asset.joints?.some(j => j.body1.part === part && j.repeat)) return nothing
|
|
1065
|
+
const from = this.pitchInputOf(asset, part)
|
|
1066
|
+
const listed = from ? asset.publicInputs?.find(p => p.input === from) : undefined
|
|
1067
|
+
const input = from ? asset.document.model.inputs.find(i => i.id === from) : undefined
|
|
1068
|
+
const form =
|
|
1069
|
+
this.publishing?.part === part
|
|
1070
|
+
? this.publishing
|
|
1071
|
+
: listed && input
|
|
1072
|
+
? { part, label: listed.label, min: input.min, max: input.max }
|
|
1073
|
+
: undefined
|
|
1074
|
+
const open = !!form
|
|
1075
|
+
const edit = (key: 'label' | 'min' | 'max') => (e: Event) => {
|
|
1076
|
+
const target = e.target as HTMLInputElement
|
|
1077
|
+
const base = form ?? { part, label: '', min: 0, max: 0 }
|
|
1078
|
+
this.publishing = { ...base, [key]: key === 'label' ? target.value : target.valueAsNumber }
|
|
1079
|
+
}
|
|
1080
|
+
const steps = (): V3ProposalStep[] => {
|
|
1081
|
+
const f = form!
|
|
1082
|
+
const id = from ?? `${part}.pitch`
|
|
1083
|
+
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[]
|
|
1084
|
+
const exists = asset.document.model.inputs.some(i => i.id === id)
|
|
1085
|
+
return [
|
|
1086
|
+
...(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 }]),
|
|
1087
|
+
...(from ? [] : [{ kind: 'set-repeat', part, pitch: { from: { of: 'input', input: id } } }]),
|
|
1088
|
+
{ kind: 'publish-input', input: id, label: f.label }
|
|
1089
|
+
] as V3ProposalStep[]
|
|
1090
|
+
}
|
|
1091
|
+
const run = (list: V3ProposalStep[]) => {
|
|
1092
|
+
try {
|
|
1093
|
+
this.session!.applyRecipe(list)
|
|
1094
|
+
this.publishing = undefined
|
|
1095
|
+
this.error = ''
|
|
1096
|
+
this.changed()
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
this.error = error instanceof Error ? error.message : String(error)
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return html`<div public-pitch>
|
|
1102
|
+
<label toggle>
|
|
1103
|
+
<input
|
|
1104
|
+
type="checkbox"
|
|
1105
|
+
aria-label="figure.label.board-sets"
|
|
1106
|
+
.checked=${open}
|
|
1107
|
+
?disabled=${unavailable}
|
|
1108
|
+
@change=${(e: Event) =>
|
|
1109
|
+
(e.target as HTMLInputElement).checked
|
|
1110
|
+
? (this.publishing = { part, label: i18next.t('figure.label.repeat-gap'), min: Math.max(1, Math.round(pitch / 2)), max: Math.round(pitch * 2) })
|
|
1111
|
+
: listed
|
|
1112
|
+
? run([{ kind: 'unpublish-input', input: from }, { kind: 'set-repeat', part, pitch }] as V3ProposalStep[])
|
|
1113
|
+
: (this.publishing = undefined)}
|
|
1114
|
+
/>
|
|
1115
|
+
${i18next.t('figure.label.board-sets')}
|
|
1116
|
+
</label>
|
|
1117
|
+
${open
|
|
1118
|
+
? 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>
|
|
1119
|
+
<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>
|
|
1120
|
+
<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>
|
|
1121
|
+
<button act publish-pitch ?disabled=${unavailable || !form!.label.trim()} @click=${() => run(steps())}>
|
|
1122
|
+
<md-icon>${listed ? 'check' : 'ios_share'}</md-icon>${i18next.t(listed ? 'figure.button.apply' : 'figure.button.publish-input')}
|
|
1123
|
+
</button>`
|
|
1124
|
+
: nothing}
|
|
1125
|
+
</div>`
|
|
1126
|
+
}
|
|
1050
1127
|
/**
|
|
1051
1128
|
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
1052
1129
|
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
@@ -1056,6 +1133,13 @@ export class FigureV3Editor extends LitElement {
|
|
|
1056
1133
|
const input = event.target as HTMLInputElement
|
|
1057
1134
|
const pitch = input.valueAsNumber
|
|
1058
1135
|
if (!Number.isFinite(pitch) || pitch === field.value) return
|
|
1136
|
+
/* Read from a design input the board may set: the input's value changes, the row keeps reading it (ADR-0096). */
|
|
1137
|
+
const from = this.pitchInputOf(JSON.parse(this.session!.source()) as V3Asset, this.selected)
|
|
1138
|
+
if (from) {
|
|
1139
|
+
this.run(() => this.session!.apply([{ kind: 'set-design', input: from, unit: 'mm', value: pitch }]))
|
|
1140
|
+
input.value = String(this.session!.read().fields.find(i => i.id === from)?.value ?? field.value)
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1059
1143
|
try {
|
|
1060
1144
|
this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })
|
|
1061
1145
|
this.error = ''
|
|
@@ -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;
|
|
@@ -168,6 +170,13 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
168
170
|
* time); a row of a fixed count says its pitch cannot be set yet (figure-model refuses it the same way).
|
|
169
171
|
*/
|
|
170
172
|
private renderRepeat;
|
|
173
|
+
/** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
|
|
174
|
+
private pitchInputOf;
|
|
175
|
+
/**
|
|
176
|
+
* 「보드에서 바꿀 수 있음」 (ADR-0096): the row's pitch made a design input the figure publishes -- a name on the board and
|
|
177
|
+
* a range the board's value is held to -- in one step; changed or withdrawn the same way. Only a row with a record.
|
|
178
|
+
*/
|
|
179
|
+
private renderPublicPitch;
|
|
171
180
|
/**
|
|
172
181
|
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
173
182
|
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
@@ -1049,6 +1049,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1049
1049
|
})
|
|
1050
1050
|
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
1051
1051
|
</p>
|
|
1052
|
+
${this.renderPublicPitch(asset, pitch ?? 0, unavailable)}
|
|
1052
1053
|
${asset.joints?.some(j => j.body1.part === this.selected && j.repeat)
|
|
1053
1054
|
? html `<button act ?disabled=${unavailable} @click=${() => this.run(() => this.session.command({ kind: 'remove-repeat', part: this.selected }))}>
|
|
1054
1055
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-repeat')}
|
|
@@ -1056,6 +1057,81 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1056
1057
|
: nothing}
|
|
1057
1058
|
</section>`;
|
|
1058
1059
|
}
|
|
1060
|
+
/** The design input a row's pitch is read from, when it is read from one (ADR-0096). */
|
|
1061
|
+
pitchInputOf(asset, part) {
|
|
1062
|
+
const pitch = asset.joints?.find(j => j.body1.part === part && j.repeat)?.repeat?.pitch;
|
|
1063
|
+
return typeof pitch === 'object' && pitch?.from?.of === 'input' ? pitch.from.input : undefined;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* 「보드에서 바꿀 수 있음」 (ADR-0096): the row's pitch made a design input the figure publishes -- a name on the board and
|
|
1067
|
+
* a range the board's value is held to -- in one step; changed or withdrawn the same way. Only a row with a record.
|
|
1068
|
+
*/
|
|
1069
|
+
renderPublicPitch(asset, pitch, unavailable) {
|
|
1070
|
+
const part = this.selected;
|
|
1071
|
+
if (!asset.joints?.some(j => j.body1.part === part && j.repeat))
|
|
1072
|
+
return nothing;
|
|
1073
|
+
const from = this.pitchInputOf(asset, part);
|
|
1074
|
+
const listed = from ? asset.publicInputs?.find(p => p.input === from) : undefined;
|
|
1075
|
+
const input = from ? asset.document.model.inputs.find(i => i.id === from) : undefined;
|
|
1076
|
+
const form = this.publishing?.part === part
|
|
1077
|
+
? this.publishing
|
|
1078
|
+
: listed && input
|
|
1079
|
+
? { part, label: listed.label, min: input.min, max: input.max }
|
|
1080
|
+
: undefined;
|
|
1081
|
+
const open = !!form;
|
|
1082
|
+
const edit = (key) => (e) => {
|
|
1083
|
+
const target = e.target;
|
|
1084
|
+
const base = form ?? { part, label: '', min: 0, max: 0 };
|
|
1085
|
+
this.publishing = { ...base, [key]: key === 'label' ? target.value : target.valueAsNumber };
|
|
1086
|
+
};
|
|
1087
|
+
const steps = () => {
|
|
1088
|
+
const f = form;
|
|
1089
|
+
const id = from ?? `${part}.pitch`;
|
|
1090
|
+
if (listed)
|
|
1091
|
+
return [{ kind: 'set-input-range', input: id, min: f.min, max: f.max }, { kind: 'publish-input', input: id, label: f.label }];
|
|
1092
|
+
const exists = asset.document.model.inputs.some(i => i.id === id);
|
|
1093
|
+
return [
|
|
1094
|
+
...(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 }]),
|
|
1095
|
+
...(from ? [] : [{ kind: 'set-repeat', part, pitch: { from: { of: 'input', input: id } } }]),
|
|
1096
|
+
{ kind: 'publish-input', input: id, label: f.label }
|
|
1097
|
+
];
|
|
1098
|
+
};
|
|
1099
|
+
const run = (list) => {
|
|
1100
|
+
try {
|
|
1101
|
+
this.session.applyRecipe(list);
|
|
1102
|
+
this.publishing = undefined;
|
|
1103
|
+
this.error = '';
|
|
1104
|
+
this.changed();
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
return html `<div public-pitch>
|
|
1111
|
+
<label toggle>
|
|
1112
|
+
<input
|
|
1113
|
+
type="checkbox"
|
|
1114
|
+
aria-label="figure.label.board-sets"
|
|
1115
|
+
.checked=${open}
|
|
1116
|
+
?disabled=${unavailable}
|
|
1117
|
+
@change=${(e) => e.target.checked
|
|
1118
|
+
? (this.publishing = { part, label: i18next.t('figure.label.repeat-gap'), min: Math.max(1, Math.round(pitch / 2)), max: Math.round(pitch * 2) })
|
|
1119
|
+
: listed
|
|
1120
|
+
? run([{ kind: 'unpublish-input', input: from }, { kind: 'set-repeat', part, pitch }])
|
|
1121
|
+
: (this.publishing = undefined)}
|
|
1122
|
+
/>
|
|
1123
|
+
${i18next.t('figure.label.board-sets')}
|
|
1124
|
+
</label>
|
|
1125
|
+
${open
|
|
1126
|
+
? 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>
|
|
1127
|
+
<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>
|
|
1128
|
+
<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>
|
|
1129
|
+
<button act publish-pitch ?disabled=${unavailable || !form.label.trim()} @click=${() => run(steps())}>
|
|
1130
|
+
<md-icon>${listed ? 'check' : 'ios_share'}</md-icon>${i18next.t(listed ? 'figure.button.apply' : 'figure.button.publish-input')}
|
|
1131
|
+
</button>`
|
|
1132
|
+
: nothing}
|
|
1133
|
+
</div>`;
|
|
1134
|
+
}
|
|
1059
1135
|
/**
|
|
1060
1136
|
* A pitch always goes through `set-repeat`: the command moves the longest length with it (at most `limit` copies),
|
|
1061
1137
|
* which a plain input edit would not. Refused only when the authored length would already hold more copies than
|
|
@@ -1066,6 +1142,13 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1066
1142
|
const pitch = input.valueAsNumber;
|
|
1067
1143
|
if (!Number.isFinite(pitch) || pitch === field.value)
|
|
1068
1144
|
return;
|
|
1145
|
+
/* Read from a design input the board may set: the input's value changes, the row keeps reading it (ADR-0096). */
|
|
1146
|
+
const from = this.pitchInputOf(JSON.parse(this.session.source()), this.selected);
|
|
1147
|
+
if (from) {
|
|
1148
|
+
this.run(() => this.session.apply([{ kind: 'set-design', input: from, unit: 'mm', value: pitch }]));
|
|
1149
|
+
input.value = String(this.session.read().fields.find(i => i.id === from)?.value ?? field.value);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1069
1152
|
try {
|
|
1070
1153
|
this.session.command({ kind: 'set-repeat', part: this.selected, pitch });
|
|
1071
1154
|
this.error = '';
|
|
@@ -1353,6 +1436,10 @@ __decorate([
|
|
|
1353
1436
|
property({ attribute: false }),
|
|
1354
1437
|
__metadata("design:type", Function)
|
|
1355
1438
|
], FigureV3Editor.prototype, "thumbnailOf", void 0);
|
|
1439
|
+
__decorate([
|
|
1440
|
+
state(),
|
|
1441
|
+
__metadata("design:type", Object)
|
|
1442
|
+
], FigureV3Editor.prototype, "publishing", void 0);
|
|
1356
1443
|
__decorate([
|
|
1357
1444
|
state(),
|
|
1358
1445
|
__metadata("design:type", Object)
|