@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.
@@ -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)