@things-factory/figure-ui 10.1.112 → 10.1.114

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.
Files changed (32) hide show
  1. package/client/modeller/editor.ts +39 -5
  2. package/client/modeller/face-mapping.ts +39 -0
  3. package/client/modeller/figure-face-mapping.ts +78 -0
  4. package/client/modeller/figure-section-editor.ts +12 -4
  5. package/client/modeller/figure-section-panel.ts +5 -1
  6. package/dist-client/modeller/clips.d.ts +1 -1
  7. package/dist-client/modeller/editor.d.ts +4 -0
  8. package/dist-client/modeller/editor.js +40 -5
  9. package/dist-client/modeller/editor.js.map +1 -1
  10. package/dist-client/modeller/face-mapping.d.ts +15 -0
  11. package/dist-client/modeller/face-mapping.js +20 -0
  12. package/dist-client/modeller/face-mapping.js.map +1 -0
  13. package/dist-client/modeller/figure-face-mapping.d.ts +16 -0
  14. package/dist-client/modeller/figure-face-mapping.js +85 -0
  15. package/dist-client/modeller/figure-face-mapping.js.map +1 -0
  16. package/dist-client/modeller/figure-section-editor.d.ts +4 -1
  17. package/dist-client/modeller/figure-section-editor.js +14 -3
  18. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  19. package/dist-client/modeller/figure-section-panel.d.ts +4 -1
  20. package/dist-client/modeller/figure-section-panel.js +6 -0
  21. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  22. package/dist-client/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +5 -5
  24. package/test/browser/acceptance-page.js +11 -0
  25. package/test/clip-consent.test.ts +1 -1
  26. package/test/face-mapping.test.ts +80 -0
  27. package/test/section-panel.test.ts +1 -1
  28. package/translations/en.json +7 -1
  29. package/translations/ja.json +7 -1
  30. package/translations/ko.json +7 -1
  31. package/translations/ms.json +7 -1
  32. package/translations/zh.json +7 -1
@@ -9,7 +9,7 @@ import { live } from 'lit/directives/live.js'
9
9
  import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, parseFigureAsset, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact, type FigureClipLoss, figurePartFaceOf, type FigureFaceArea, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model'
10
10
  import type { RecipeTemplate } from './recipes.js'
11
11
  import './figure-recipe-catalog.js'
12
- import type { FigureAsset, FigureAuthoringAction, FigureEvaluatedShape, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement } from '@hatiolab/figure-model'
12
+ import type { FigureAsset, FigureAuthoringAction, FigureEvaluatedShape, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement, FigureFaceMappingNeed, FigureFaceRemap } from '@hatiolab/figure-model'
13
13
  import type { FigureProductSession } from './product-session.js'
14
14
  import { captureFigureThumbnail } from './thumbnail.js'
15
15
  import { drawableFigureParts } from './renderer.js'
@@ -396,6 +396,12 @@ export class FigureEditor extends LitElement {
396
396
  @property({ attribute: false }) firstSave?: (name: string) => Promise<FirstSave | undefined>
397
397
  @state() private selected = ''
398
398
  @state() private error = ''
399
+ /*
400
+ * An edit that takes a face away which something stands on (ADR-0100 A8): held, with what the kernel says stands
401
+ * there, until the author chooses where each goes -- then the same edit goes again with the remap. Shown where the
402
+ * edit was made (`from`).
403
+ */
404
+ @state() private faceMapping?: { action: FigureAuthoringAction; need: FigureFaceMappingNeed; from: 'section-editor' | 'panel' }
399
405
  /** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
400
406
  @state() private retry?: { label: string; action?: FigureAuthoringAction; run?: () => void }
401
407
  @state() private tab: SideTab = 'properties'
@@ -1168,7 +1174,15 @@ export class FigureEditor extends LitElement {
1168
1174
  ></figure-clips>`
1169
1175
  }
1170
1176
  /** One command, one step of the history. A refusal a person can meet here is said in their words. */
1171
- private linkCommand(action: FigureAuthoringAction) {
1177
+ /** Where a held edit goes once the author has chosen where what stands on its lost faces goes. */
1178
+ private mapFaces(remap: FigureFaceRemap[]) {
1179
+ const held = this.faceMapping
1180
+ if (!held) return
1181
+ const done = this.linkCommand({ ...held.action, remap } as FigureAuthoringAction, held.from)
1182
+ if (done && held.from === 'section-editor') this.closeSectionEditor()
1183
+ }
1184
+ /** Runs a command; true when it was taken. */
1185
+ private linkCommand(action: FigureAuthoringAction, from: 'section-editor' | 'panel' = 'panel'): boolean {
1172
1186
  /* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
1173
1187
  this.holdAction(null)
1174
1188
  this.retry = undefined
@@ -1177,6 +1191,7 @@ export class FigureEditor extends LitElement {
1177
1191
  try {
1178
1192
  this.session!.command(action)
1179
1193
  this.error = ''
1194
+ this.faceMapping = undefined
1180
1195
  /* What the command did besides what was asked, in one line: the figure's size declared to follow (ADR ruling Q1). */
1181
1196
  if (action.kind === 'follow-size' && !hadSize) {
1182
1197
  const size = this.designValues()
@@ -1187,14 +1202,22 @@ export class FigureEditor extends LitElement {
1187
1202
  })
1188
1203
  }
1189
1204
  this.changed()
1205
+ return true
1190
1206
  } catch (error) {
1191
- if (this.askClipLoss(error, () => this.linkCommand(action))) return
1207
+ if (this.askClipLoss(error, () => this.linkCommand(action, from))) return false
1192
1208
  const code = (error as { code?: string })?.code
1209
+ /* Something stands on a face this takes away: ask where it goes, there, rather than only saying no. */
1210
+ const need = (error as { details?: FigureFaceMappingNeed }).details
1211
+ if (code === 'FEATURE_MAPPING_REQUIRED' && need?.lost?.length) {
1212
+ this.faceMapping = { action, need, from }
1213
+ this.error = ''
1214
+ return false
1215
+ }
1193
1216
  const narrowing = narrowingRetryOf(action, error, (key, params) => i18next.t(key, params))
1194
1217
  if (narrowing) {
1195
1218
  this.error = narrowing.message
1196
1219
  this.retry = { label: narrowing.label, action: narrowing.retry as FigureAuthoringAction }
1197
- return
1220
+ return false
1198
1221
  }
1199
1222
  this.error =
1200
1223
  code === 'DIMENSION_CYCLE'
@@ -1204,6 +1227,7 @@ export class FigureEditor extends LitElement {
1204
1227
  : sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ??
1205
1228
  placementRefusalOf(error, (key, params) => i18next.t(key, params)) ??
1206
1229
  this.said(error)
1230
+ return false
1207
1231
  }
1208
1232
  }
1209
1233
  private changeField(field: { id: string; unit: FigureInputChange['unit']; role?: string }, event: Event) {
@@ -1743,6 +1767,7 @@ export class FigureEditor extends LitElement {
1743
1767
  private choose(id: string) {
1744
1768
  /* A copy on the canvas selects its repeat: the part the author made. */
1745
1769
  this.selected = id ? ownerOf(id) : id
1770
+ this.faceMapping = undefined
1746
1771
  if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history')) this.tab = 'properties'
1747
1772
  }
1748
1773
  private asset(): unknown {
@@ -1801,8 +1826,14 @@ export class FigureEditor extends LitElement {
1801
1826
  .kept=${kept}
1802
1827
  .pointBlocked=${this.pointBlockedOf(drawn)}
1803
1828
  .revision=${this.session?.read().revision ?? 0}
1804
- @command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
1829
+ .faceMapping=${this.faceMapping?.from === 'section-editor' ? this.faceMapping.need : undefined}
1830
+ @command=${(e: CustomEvent) => {
1831
+ /* Held for a choice of where things go: the editor stays open, the choice shown in it. */
1832
+ if (!this.linkCommand(e.detail.action, 'section-editor')) e.detail.held = true
1833
+ }}
1805
1834
  @command-live=${(e: CustomEvent) => this.holdAction(e.detail.action)}
1835
+ @face-mapped=${(e: CustomEvent) => this.mapFaces(e.detail.remap)}
1836
+ @face-mapping-cancel=${() => (this.faceMapping = undefined)}
1806
1837
  @section-editor-close=${() => this.closeSectionEditor()}
1807
1838
  ></figure-section-editor>`
1808
1839
  }
@@ -2078,6 +2109,9 @@ export class FigureEditor extends LitElement {
2078
2109
  <figure-section-panel
2079
2110
  .body=${{ ...part, id: this.selected }}
2080
2111
  ?locked=${unavailable}
2112
+ .faceMapping=${this.faceMapping?.from === 'panel' ? this.faceMapping.need : undefined}
2113
+ @face-mapped=${(e: CustomEvent) => this.mapFaces(e.detail.remap)}
2114
+ @face-mapping-cancel=${() => (this.faceMapping = undefined)}
2081
2115
  @command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
2082
2116
  @command-live=${(e: CustomEvent) => this.holdAction(e.detail.action)}
2083
2117
  @section-editor-open=${() => (this.sectionEditing = this.selected)}
@@ -0,0 +1,39 @@
1
+ import type { FigureFaceMappingNeed, FigureFaceReference, FigureFaceRemap } from '@hatiolab/figure-model'
2
+
3
+ /*
4
+ * Where what stands on a face an edit takes away goes instead (ADR-0100 A8, user 2026-10-02): one row per thing that
5
+ * refers to a lost face, its choices the faces that remain looking the same way, and -- for a joint, never a named
6
+ * face -- let go where it stands. Nothing is chosen for the author: where it goes only they know.
7
+ */
8
+
9
+ export interface FaceMappingRow {
10
+ ref: FigureFaceReference
11
+ /** The face it is on now, which the edit takes away. */
12
+ lost: string
13
+ /** The faces it may go to: those that remain, looking the way the lost one did. */
14
+ faces: string[]
15
+ /** Whether it may be let go where it stands: a joint may, a named face may not. */
16
+ detach: boolean
17
+ }
18
+
19
+ export const DETACH = '__detach'
20
+
21
+ export function faceMappingRowsOf(need: FigureFaceMappingNeed): FaceMappingRow[] {
22
+ return need.lost.flatMap(lost => {
23
+ const faces = need.remain.filter(r => lost.axis === undefined || (r.axis === lost.axis && r.normal === lost.normal)).map(r => r.face)
24
+ return lost.refs.map(ref => ({ ref, lost: lost.face, faces, detach: ref.kind === 'joint' }))
25
+ })
26
+ }
27
+
28
+ /** The remap a command takes, once every row is chosen; undefined while one is not. */
29
+ export function remapOf(rows: readonly FaceMappingRow[], picks: Readonly<Record<string, string>>): FigureFaceRemap[] | undefined {
30
+ const out: FigureFaceRemap[] = []
31
+ for (const row of rows) {
32
+ const pick = picks[rowKey(row)]
33
+ if (!pick) return undefined
34
+ out.push({ ref: { kind: row.ref.kind, id: row.ref.id }, to: pick === DETACH ? 'detach' : { face: pick } })
35
+ }
36
+ return out
37
+ }
38
+
39
+ export const rowKey = (row: FaceMappingRow) => `${row.ref.kind}:${row.ref.id}:${row.lost}`
@@ -0,0 +1,78 @@
1
+ import { css, html, LitElement, nothing, type PropertyValues } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+
4
+ import { i18next, localize } from '@operato/i18n'
5
+ import type { FigureFaceMappingNeed } from '@hatiolab/figure-model'
6
+
7
+ import { SidePanelStyles } from './side-panel-styles.js'
8
+ import { DETACH, faceMappingRowsOf, remapOf, rowKey } from './face-mapping.js'
9
+
10
+ /**
11
+ * 없어지는 면에 붙은 것을 옮길 곳 (ADR-0100 A8): shown where the edit was made, under it. Each thing on a lost face
12
+ * is a row; apply is held until every row is chosen, and then the same edit goes again with the remap, one step.
13
+ */
14
+ @customElement('figure-face-mapping')
15
+ export class FigureFaceMapping extends localize(i18next)(LitElement) {
16
+ static styles = [
17
+ SidePanelStyles,
18
+ css`
19
+ :host {
20
+ display: grid;
21
+ gap: 8px;
22
+ padding: 10px 12px;
23
+ border: 1px solid var(--md-sys-color-error);
24
+ border-radius: 8px;
25
+ }
26
+ div[row] {
27
+ display: grid;
28
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
29
+ gap: 8px;
30
+ align-items: center;
31
+ }
32
+ div[buttons] {
33
+ display: flex;
34
+ gap: 8px;
35
+ justify-content: flex-end;
36
+ }
37
+ `
38
+ ]
39
+
40
+ @property({ attribute: false }) need?: FigureFaceMappingNeed
41
+ @state() private picks: Record<string, string> = {}
42
+
43
+ protected willUpdate(changed: PropertyValues<this>) {
44
+ /* Another refusal is another question: what was chosen for the last one does not carry over. */
45
+ if (changed.has('need')) this.picks = {}
46
+ }
47
+
48
+ private face = (face: string) => i18next.t('figure.label.face-' + face, { defaultValue: face })
49
+
50
+ render() {
51
+ if (!this.need) return nothing
52
+ const rows = faceMappingRowsOf(this.need)
53
+ const remap = remapOf(rows, this.picks)
54
+ const lost = [...new Set(this.need.lost.map(l => this.face(l.face)))].join(', ')
55
+ return html`<p>${i18next.t('figure.text.face-mapping-lost', { faces: lost })}</p>
56
+ ${rows.map(
57
+ row => html`<div row>
58
+ <span>${row.ref.part ?? row.ref.id} · ${this.face(row.lost)}</span>
59
+ <select
60
+ aria-label=${i18next.t('figure.label.face-mapping-to', { part: row.ref.part ?? row.ref.id })}
61
+ @change=${(e: Event) => (this.picks = { ...this.picks, [rowKey(row)]: (e.target as HTMLSelectElement).value })}
62
+ >
63
+ <option value="" ?selected=${!this.picks[rowKey(row)]}>${i18next.t('figure.label.face-mapping-choose')}</option>
64
+ ${row.faces.map(f => html`<option value=${f}>${this.face(f)}</option>`)}
65
+ ${row.detach ? html`<option value=${DETACH}>${i18next.t('figure.label.face-mapping-detach')}</option>` : nothing}
66
+ </select>
67
+ </div>`
68
+ )}
69
+ <div buttons>
70
+ <button @click=${() => this.dispatchEvent(new CustomEvent('face-mapping-cancel', { bubbles: true, composed: true }))}>
71
+ ${i18next.t('figure.button.face-mapping-cancel')}
72
+ </button>
73
+ <button act ?disabled=${!remap} @click=${() => remap && this.dispatchEvent(new CustomEvent('face-mapped', { detail: { remap }, bubbles: true, composed: true }))}>
74
+ ${i18next.t('figure.button.face-mapping-apply')}
75
+ </button>
76
+ </div>`
77
+ }
78
+ }
@@ -4,11 +4,12 @@ import { live } from 'lit/directives/live.js'
4
4
 
5
5
  import { i18next, localize } from '@operato/i18n'
6
6
  import { figureCavitySectionOf, figureSectionSidesOf } from '@hatiolab/figure-model'
7
- import type { FigureAuthoringAction, FigureEvaluatedShape, FigureSection, FigureSectionMeasure, FigureSectionPoint } from '@hatiolab/figure-model'
7
+ import type { FigureAuthoringAction, FigureEvaluatedShape, FigureFaceMappingNeed, FigureSection, FigureSectionMeasure, FigureSectionPoint } from '@hatiolab/figure-model'
8
8
  import { SidePanelStyles } from './side-panel-styles.js'
9
9
  import { apexOver, drawnOf, insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf, workingShapeOf, type SectionWorking } from './section-drawing.js'
10
10
  import { cavityDepthOf, cavityRequestOf, cavityResultOf, END_FACE, sameOf, sidesOf } from './section-controls.js'
11
11
  import './figure-cavity-drawing.js'
12
+ import './figure-face-mapping.js'
12
13
  import { openPopup } from '@operato/layout'
13
14
  import './figure-section-close-choice.js'
14
15
  import type { SectionCloseChoice } from './figure-section-close-choice.js'
@@ -276,6 +277,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
276
277
  @property({ attribute: false }) pointBlocked = ''
277
278
  /** The figure's revision now; the editor remembers the one it opened at. */
278
279
  @property({ type: Number }) revision = 0
280
+ /** What stands on the faces the last apply takes away (ADR-0100 A8): asked here, where the edit was made. */
281
+ @property({ attribute: false }) faceMapping?: FigureFaceMappingNeed
279
282
  /** Narrow: the outline or the solid behind. */
280
283
  @property({ reflect: true }) show: 'outline' | 'solid' = 'outline'
281
284
  /** Set when the space the editor stands in leaves too little beside it to see the solid. */
@@ -430,8 +433,10 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
430
433
 
431
434
  private apply() {
432
435
  if (this.stale || !this.valid(this.working)) return
433
- this.dispatchEvent(new CustomEvent('command', { detail: { action: this.action(this.working) }, bubbles: true, composed: true }))
434
- this.close()
436
+ const detail: { action: FigureAuthoringAction; held?: boolean } = { action: this.action(this.working) }
437
+ this.dispatchEvent(new CustomEvent('command', { detail, bubbles: true, composed: true }))
438
+ /* Held for a choice of where what stands on a lost face goes: stays open, the choice shown below. */
439
+ if (!detail.held) this.close()
435
440
  }
436
441
 
437
442
  /** Whether anything was changed since the editor opened. */
@@ -526,7 +531,10 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
526
531
  </nav>
527
532
  <div body>
528
533
  <div outline>${this.drawing(issues, words.length > 0)}</div>
529
- <aside>${this.list(issues, words)}</aside>
534
+ <aside>
535
+ ${this.faceMapping ? html`<figure-face-mapping .need=${this.faceMapping}></figure-face-mapping>` : nothing}
536
+ ${this.list(issues, words)}
537
+ </aside>
530
538
  </div>
531
539
  <footer>
532
540
  <button ?disabled=${!this.history.length} @click=${() => this.back()}>${i18next.t('figure.button.section-editor-back')}</button>
@@ -4,10 +4,11 @@ import { customElement, property, state } from 'lit/decorators.js'
4
4
  import { live } from 'lit/directives/live.js'
5
5
 
6
6
  import { i18next, localize } from '@operato/i18n'
7
- import type { FigureAuthoringAction, FigureEvaluatedShape } from '@hatiolab/figure-model'
7
+ import type { FigureAuthoringAction, FigureEvaluatedShape, FigureFaceMappingNeed } from '@hatiolab/figure-model'
8
8
  import { SidePanelStyles } from './side-panel-styles.js'
9
9
  import { drawnOf } from './section-drawing.js'
10
10
  import './figure-cavity-drawing.js'
11
+ import './figure-face-mapping.js'
11
12
  import { cavityDepthOf, cavityRequestOf, cavityResultOf, cornerLimitOf, cornerRefusalOf, mm, sameOf } from './section-controls.js'
12
13
 
13
14
  type Body = { id: string; shape: FigureEvaluatedShape }
@@ -59,6 +60,8 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
59
60
 
60
61
  @property({ attribute: false }) body?: Body
61
62
  @property({ type: Boolean }) locked = false
63
+ /** What stands on the faces the last edit here takes away (ADR-0100 A8): asked here, where the edit was made. */
64
+ @property({ attribute: false }) faceMapping?: FigureFaceMappingNeed
62
65
  /** What each field would not take, by field, said under it. */
63
66
  @state() private refusals: Record<string, string> = {}
64
67
  /** What is typed in a field and not yet sent, by field: the figure drawn with it must not type over it. */
@@ -97,6 +100,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
97
100
  : nothing}
98
101
  ${kind === 'rectangle' ? this.renderCorner(body) : nothing}
99
102
  ${/* No inside for a drawn section: the control is not there, rather than a line saying so. */ kind === 'rectangle' || kind === 'ellipse' ? this.renderCavity(body) : nothing}
103
+ ${this.faceMapping ? html`<figure-face-mapping .need=${this.faceMapping}></figure-face-mapping>` : nothing}
100
104
  </section>`
101
105
  }
102
106
 
@@ -34,6 +34,6 @@ export declare const trackEdit: (clip: Clip, track: ClipTrack, patch?: Partial<P
34
34
  state: string;
35
35
  clip: string;
36
36
  keys: ClipKey[];
37
- loop: "hold" | "wrap";
37
+ loop: "wrap" | "hold";
38
38
  interpolation: "linear" | "step";
39
39
  };
@@ -47,6 +47,7 @@ export declare class FigureEditor extends LitElement {
47
47
  firstSave?: (name: string) => Promise<FirstSave | undefined>;
48
48
  private selected;
49
49
  private error;
50
+ private faceMapping?;
50
51
  /** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
51
52
  private retry?;
52
53
  private tab;
@@ -210,6 +211,9 @@ export declare class FigureEditor extends LitElement {
210
211
  /** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
211
212
  private renderControls;
212
213
  /** One command, one step of the history. A refusal a person can meet here is said in their words. */
214
+ /** Where a held edit goes once the author has chosen where what stands on its lost faces goes. */
215
+ private mapFaces;
216
+ /** Runs a command; true when it was taken. */
213
217
  private linkCommand;
214
218
  private changeField;
215
219
  /** Typing or a held spinner: each number on the way is drawn; the field's change records it. */
@@ -1194,7 +1194,17 @@ let FigureEditor = class FigureEditor extends LitElement {
1194
1194
  ></figure-clips>`;
1195
1195
  }
1196
1196
  /** One command, one step of the history. A refusal a person can meet here is said in their words. */
1197
- linkCommand(action) {
1197
+ /** Where a held edit goes once the author has chosen where what stands on its lost faces goes. */
1198
+ mapFaces(remap) {
1199
+ const held = this.faceMapping;
1200
+ if (!held)
1201
+ return;
1202
+ const done = this.linkCommand({ ...held.action, remap }, held.from);
1203
+ if (done && held.from === 'section-editor')
1204
+ this.closeSectionEditor();
1205
+ }
1206
+ /** Runs a command; true when it was taken. */
1207
+ linkCommand(action, from = 'panel') {
1198
1208
  /* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
1199
1209
  this.holdAction(null);
1200
1210
  this.retry = undefined;
@@ -1203,6 +1213,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1203
1213
  try {
1204
1214
  this.session.command(action);
1205
1215
  this.error = '';
1216
+ this.faceMapping = undefined;
1206
1217
  /* What the command did besides what was asked, in one line: the figure's size declared to follow (ADR ruling Q1). */
1207
1218
  if (action.kind === 'follow-size' && !hadSize) {
1208
1219
  const size = this.designValues();
@@ -1213,16 +1224,24 @@ let FigureEditor = class FigureEditor extends LitElement {
1213
1224
  });
1214
1225
  }
1215
1226
  this.changed();
1227
+ return true;
1216
1228
  }
1217
1229
  catch (error) {
1218
- if (this.askClipLoss(error, () => this.linkCommand(action)))
1219
- return;
1230
+ if (this.askClipLoss(error, () => this.linkCommand(action, from)))
1231
+ return false;
1220
1232
  const code = error?.code;
1233
+ /* Something stands on a face this takes away: ask where it goes, there, rather than only saying no. */
1234
+ const need = error.details;
1235
+ if (code === 'FEATURE_MAPPING_REQUIRED' && need?.lost?.length) {
1236
+ this.faceMapping = { action, need, from };
1237
+ this.error = '';
1238
+ return false;
1239
+ }
1221
1240
  const narrowing = narrowingRetryOf(action, error, (key, params) => i18next.t(key, params));
1222
1241
  if (narrowing) {
1223
1242
  this.error = narrowing.message;
1224
1243
  this.retry = { label: narrowing.label, action: narrowing.retry };
1225
- return;
1244
+ return false;
1226
1245
  }
1227
1246
  this.error =
1228
1247
  code === 'DIMENSION_CYCLE'
@@ -1232,6 +1251,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1232
1251
  : sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ??
1233
1252
  placementRefusalOf(error, (key, params) => i18next.t(key, params)) ??
1234
1253
  this.said(error);
1254
+ return false;
1235
1255
  }
1236
1256
  }
1237
1257
  changeField(field, event) {
@@ -1779,6 +1799,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1779
1799
  choose(id) {
1780
1800
  /* A copy on the canvas selects its repeat: the part the author made. */
1781
1801
  this.selected = id ? ownerOf(id) : id;
1802
+ this.faceMapping = undefined;
1782
1803
  if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history'))
1783
1804
  this.tab = 'properties';
1784
1805
  }
@@ -1842,8 +1863,15 @@ let FigureEditor = class FigureEditor extends LitElement {
1842
1863
  .kept=${kept}
1843
1864
  .pointBlocked=${this.pointBlockedOf(drawn)}
1844
1865
  .revision=${this.session?.read().revision ?? 0}
1845
- @command=${(e) => this.linkCommand(e.detail.action)}
1866
+ .faceMapping=${this.faceMapping?.from === 'section-editor' ? this.faceMapping.need : undefined}
1867
+ @command=${(e) => {
1868
+ /* Held for a choice of where things go: the editor stays open, the choice shown in it. */
1869
+ if (!this.linkCommand(e.detail.action, 'section-editor'))
1870
+ e.detail.held = true;
1871
+ }}
1846
1872
  @command-live=${(e) => this.holdAction(e.detail.action)}
1873
+ @face-mapped=${(e) => this.mapFaces(e.detail.remap)}
1874
+ @face-mapping-cancel=${() => (this.faceMapping = undefined)}
1847
1875
  @section-editor-close=${() => this.closeSectionEditor()}
1848
1876
  ></figure-section-editor>`;
1849
1877
  }
@@ -2115,6 +2143,9 @@ let FigureEditor = class FigureEditor extends LitElement {
2115
2143
  <figure-section-panel
2116
2144
  .body=${{ ...part, id: this.selected }}
2117
2145
  ?locked=${unavailable}
2146
+ .faceMapping=${this.faceMapping?.from === 'panel' ? this.faceMapping.need : undefined}
2147
+ @face-mapped=${(e) => this.mapFaces(e.detail.remap)}
2148
+ @face-mapping-cancel=${() => (this.faceMapping = undefined)}
2118
2149
  @command=${(e) => this.linkCommand(e.detail.action)}
2119
2150
  @command-live=${(e) => this.holdAction(e.detail.action)}
2120
2151
  @section-editor-open=${() => (this.sectionEditing = this.selected)}
@@ -2363,6 +2394,10 @@ __decorate([
2363
2394
  state(),
2364
2395
  __metadata("design:type", Object)
2365
2396
  ], FigureEditor.prototype, "error", void 0);
2397
+ __decorate([
2398
+ state(),
2399
+ __metadata("design:type", Object)
2400
+ ], FigureEditor.prototype, "faceMapping", void 0);
2366
2401
  __decorate([
2367
2402
  state(),
2368
2403
  __metadata("design:type", Object)