@things-factory/figure-ui 10.1.151 → 10.1.153
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/ai-proposal.ts +14 -13
- package/client/modeller/command-preview-controller.ts +210 -0
- package/client/modeller/editor-styles.ts +307 -0
- package/client/modeller/editor.ts +65 -634
- package/client/modeller/fasten-geometry.ts +32 -0
- package/client/modeller/figure-first-save.ts +9 -1
- package/client/modeller/figure-links.ts +32 -14
- package/client/modeller/mesh-import.ts +0 -10
- package/client/modeller/modeller-keys.ts +60 -0
- package/client/modeller/pane-fold.ts +61 -0
- package/client/modeller/panel-memory.ts +53 -0
- package/client/modeller/refusal-teller.ts +93 -0
- package/client/modeller/refusals.ts +32 -2
- package/dist-client/modeller/ai-proposal.d.ts +6 -5
- package/dist-client/modeller/ai-proposal.js +9 -9
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/command-preview-controller.d.ts +75 -0
- package/dist-client/modeller/command-preview-controller.js +178 -0
- package/dist-client/modeller/command-preview-controller.js.map +1 -0
- package/dist-client/modeller/editor-styles.d.ts +2 -0
- package/dist-client/modeller/editor-styles.js +307 -0
- package/dist-client/modeller/editor-styles.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +5 -56
- package/dist-client/modeller/editor.js +76 -656
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +13 -0
- package/dist-client/modeller/fasten-geometry.js +29 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +1 -0
- package/dist-client/modeller/figure-first-save.js +8 -1
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -2
- package/dist-client/modeller/figure-links.js +35 -14
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +0 -2
- package/dist-client/modeller/mesh-import.js +0 -11
- package/dist-client/modeller/mesh-import.js.map +1 -1
- package/dist-client/modeller/modeller-keys.d.ts +22 -0
- package/dist-client/modeller/modeller-keys.js +43 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -0
- package/dist-client/modeller/pane-fold.d.ts +15 -0
- package/dist-client/modeller/pane-fold.js +60 -0
- package/dist-client/modeller/pane-fold.js.map +1 -0
- package/dist-client/modeller/panel-memory.d.ts +15 -0
- package/dist-client/modeller/panel-memory.js +54 -0
- package/dist-client/modeller/panel-memory.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +4 -28
- package/dist-client/modeller/refusal-teller.d.ts +36 -0
- package/dist-client/modeller/refusal-teller.js +81 -0
- package/dist-client/modeller/refusal-teller.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +12 -1
- package/dist-client/modeller/refusals.js +26 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/browser/build.mjs +7 -2
- package/test/browser/editing.check.mjs +14 -0
- package/test/browser/fix-alignment.check.mjs +112 -0
- package/test/browser/links.check.mjs +69 -13
- package/test/browser/mesh-select.check.mjs +91 -0
- package/test/browser/modeller-page.js +36 -0
- package/test/browser/modeller-revision.check.mjs +18 -6
- package/test/browser/modeller.check.mjs +25 -0
- package/test/browser/part-ids.check.mjs +24 -6
- package/test/browser/parts-page.js +9 -1
- package/test/browser/parts.check.mjs +61 -2
- package/test/browser/places-mesh.check.mjs +25 -9
- package/test/browser/product-page.check.mjs +42 -2
- package/test/browser/row-preview.check.mjs +81 -0
- package/test/browser/screen.check.mjs +35 -1
- package/test/clip-ai-proposal.test.ts +4 -1
- package/test/clip-consent.test.ts +9 -2
- package/test/fields.test.ts +5 -9
- package/test/fix-alignment.test.ts +32 -0
- package/test/layout.test.ts +3 -1
- package/test/mesh-import.test.ts +1 -8
- package/test/parts-wiring.test.ts +1 -5
- package/test/refusals.test.ts +46 -10
- package/test/theme.test.ts +5 -15
- package/translations/en.json +8 -0
- package/translations/es.json +8 -0
- package/translations/ja.json +8 -0
- package/translations/ko.json +8 -0
- package/translations/ms.json +8 -0
- package/translations/zh.json +8 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import { type KeyAction } from './keys.js';
|
|
3
|
+
/** What the keys ask of the editor. */
|
|
4
|
+
export interface ModellerKeysHost {
|
|
5
|
+
/** Whether the modeller takes keys now: a figure open, not loading or saving, and no section editor open. */
|
|
6
|
+
taking(): boolean;
|
|
7
|
+
/** The scene, whose keys the modeller's are too. */
|
|
8
|
+
scene(): Element | null;
|
|
9
|
+
selected(): string;
|
|
10
|
+
act(action: KeyAction): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The modeller's keys (`keys.ts`): undo, redo, duplicate, remove, back to the whole figure, the gizmo's modes. Heard on
|
|
14
|
+
* the window, first (capture), while the editor is on the page.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ModellerKeys implements ReactiveController {
|
|
17
|
+
private readonly of;
|
|
18
|
+
constructor(host: ReactiveControllerHost, of: ModellerKeysHost);
|
|
19
|
+
hostConnected(): void;
|
|
20
|
+
hostDisconnected(): void;
|
|
21
|
+
private onKey;
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { keyActionOf } from './keys.js';
|
|
2
|
+
/**
|
|
3
|
+
* The modeller's keys (`keys.ts`): undo, redo, duplicate, remove, back to the whole figure, the gizmo's modes. Heard on
|
|
4
|
+
* the window, first (capture), while the editor is on the page.
|
|
5
|
+
*/
|
|
6
|
+
export class ModellerKeys {
|
|
7
|
+
constructor(host, of) {
|
|
8
|
+
this.of = of;
|
|
9
|
+
this.onKey = (e) => {
|
|
10
|
+
if (!this.of.taking())
|
|
11
|
+
return;
|
|
12
|
+
const canvas = this.of.scene();
|
|
13
|
+
const path = e.composedPath();
|
|
14
|
+
const action = keyActionOf({
|
|
15
|
+
key: e.key,
|
|
16
|
+
ctrlKey: e.ctrlKey,
|
|
17
|
+
metaKey: e.metaKey,
|
|
18
|
+
shiftKey: e.shiftKey,
|
|
19
|
+
altKey: e.altKey,
|
|
20
|
+
target: path[0],
|
|
21
|
+
inScene: !!canvas && path.includes(canvas)
|
|
22
|
+
}, this.of.selected());
|
|
23
|
+
if (!action)
|
|
24
|
+
return;
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
/*
|
|
27
|
+
The session's history is the only one. With the canvas focused the scene took Ctrl+Z as well, undoing on its own
|
|
28
|
+
board the last value the canvas had written -- which the canvas then told as a drag: an undo of a drag on a
|
|
29
|
+
part placed by a rule wrote the drag again (2026-09-25). Heard first (capture) and kept from the scene.
|
|
30
|
+
*/
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
this.of.act(action);
|
|
33
|
+
};
|
|
34
|
+
host.addController(this);
|
|
35
|
+
}
|
|
36
|
+
hostConnected() {
|
|
37
|
+
window.addEventListener('keydown', this.onKey, true);
|
|
38
|
+
}
|
|
39
|
+
hostDisconnected() {
|
|
40
|
+
window.removeEventListener('keydown', this.onKey, true);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=modeller-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modeller-keys.js","sourceRoot":"","sources":["../../client/modeller/modeller-keys.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAkB,MAAM,WAAW,CAAA;AAYvD;;;GAGG;AACH,MAAM,OAAO,YAAY;IACvB,YACE,IAA4B,EACX,EAAoB;QAApB,OAAE,GAAF,EAAE,CAAkB;QAa/B,UAAK,GAAG,CAAC,CAAgB,EAAE,EAAE;YACnC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE;gBAAE,OAAM;YAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAA;YAC9B,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,CAAA;YAC7B,MAAM,MAAM,GAAG,WAAW,CACxB;gBACE,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,MAAM,EAAE,IAAI,CAAC,CAAC,CAAgB;gBAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;aAC3C,EACD,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CACnB,CAAA;YACD,IAAI,CAAC,MAAM;gBAAE,OAAM;YACnB,CAAC,CAAC,cAAc,EAAE,CAAA;YAClB;;;;cAIE;YACF,CAAC,CAAC,eAAe,EAAE,CAAA;YACnB,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACrB,CAAC,CAAA;QApCC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAED,aAAa;QACX,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACtD,CAAC;IAED,gBAAgB;QACd,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACzD,CAAC;CA4BF","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { keyActionOf, type KeyAction } from './keys.js'\n\n/** What the keys ask of the editor. */\nexport interface ModellerKeysHost {\n /** Whether the modeller takes keys now: a figure open, not loading or saving, and no section editor open. */\n taking(): boolean\n /** The scene, whose keys the modeller's are too. */\n scene(): Element | null\n selected(): string\n act(action: KeyAction): void\n}\n\n/**\n * The modeller's keys (`keys.ts`): undo, redo, duplicate, remove, back to the whole figure, the gizmo's modes. Heard on\n * the window, first (capture), while the editor is on the page.\n */\nexport class ModellerKeys implements ReactiveController {\n constructor(\n host: ReactiveControllerHost,\n private readonly of: ModellerKeysHost\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n window.addEventListener('keydown', this.onKey, true)\n }\n\n hostDisconnected() {\n window.removeEventListener('keydown', this.onKey, true)\n }\n\n private onKey = (e: KeyboardEvent) => {\n if (!this.of.taking()) return\n const canvas = this.of.scene()\n const path = e.composedPath()\n const action = keyActionOf(\n {\n key: e.key,\n ctrlKey: e.ctrlKey,\n metaKey: e.metaKey,\n shiftKey: e.shiftKey,\n altKey: e.altKey,\n target: path[0] as HTMLElement,\n inScene: !!canvas && path.includes(canvas)\n },\n this.of.selected()\n )\n if (!action) return\n e.preventDefault()\n /*\n The session's history is the only one. With the canvas focused the scene took Ctrl+Z as well, undoing on its own\n board the last value the canvas had written -- which the canvas then told as a drag: an undo of a drag on a\n part placed by a rule wrote the drag again (2026-09-25). Heard first (capture) and kept from the scene.\n */\n e.stopPropagation()\n this.of.act(action)\n }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import type { OxThreePane } from '@operato/layout';
|
|
3
|
+
export declare class PaneFold implements ReactiveController {
|
|
4
|
+
private readonly host;
|
|
5
|
+
private readonly panes;
|
|
6
|
+
static readonly FOLD_START_UNDER = 1100;
|
|
7
|
+
private foldedForWidth;
|
|
8
|
+
private keptOpenByAuthor;
|
|
9
|
+
private widthWatch;
|
|
10
|
+
/** `panes` is the frame the bar folds, once it is drawn. */
|
|
11
|
+
constructor(host: ReactiveControllerHost & HTMLElement, panes: () => OxThreePane | undefined);
|
|
12
|
+
hostConnected(): void;
|
|
13
|
+
hostDisconnected(): void;
|
|
14
|
+
fitTo(width: number): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { i18next } from '@operato/i18n';
|
|
2
|
+
import { notify } from '@operato/layout';
|
|
3
|
+
/** Whether the narrow fold was said in this browser before; true from now on. Storage refused: said, as before. */
|
|
4
|
+
function foldSaid() {
|
|
5
|
+
try {
|
|
6
|
+
if (localStorage.getItem('figure-fold-said'))
|
|
7
|
+
return true;
|
|
8
|
+
localStorage.setItem('figure-fold-said', '1');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
/* No storage here: said every time, as it was. */
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
/*
|
|
16
|
+
Narrow, the parts list gives way to the canvas rather than all three shrinking (designer 2026-09-27, U11): under
|
|
17
|
+
FOLD_START_UNDER px the canvas would be left under about 560 px beside both sides. The fold is said once, and the
|
|
18
|
+
bar's own toggle opens it again; opened by the author, it is not folded again until the screen has been wide.
|
|
19
|
+
Said once in this browser, not on every opening (2026-10-02): the list folds as before, the notice does not come back.
|
|
20
|
+
*/
|
|
21
|
+
export class PaneFold {
|
|
22
|
+
static { this.FOLD_START_UNDER = 1100; }
|
|
23
|
+
/** `panes` is the frame the bar folds, once it is drawn. */
|
|
24
|
+
constructor(host, panes) {
|
|
25
|
+
this.host = host;
|
|
26
|
+
this.panes = panes;
|
|
27
|
+
this.foldedForWidth = false;
|
|
28
|
+
this.keptOpenByAuthor = false;
|
|
29
|
+
this.widthWatch = new ResizeObserver(([entry]) => this.fitTo(entry.contentRect.width));
|
|
30
|
+
host.addController(this);
|
|
31
|
+
}
|
|
32
|
+
hostConnected() {
|
|
33
|
+
this.widthWatch.observe(this.host);
|
|
34
|
+
}
|
|
35
|
+
hostDisconnected() {
|
|
36
|
+
this.widthWatch.disconnect();
|
|
37
|
+
}
|
|
38
|
+
fitTo(width) {
|
|
39
|
+
const panes = this.panes();
|
|
40
|
+
if (!panes || !width)
|
|
41
|
+
return;
|
|
42
|
+
if (this.foldedForWidth && !panes.foldStart)
|
|
43
|
+
this.keptOpenByAuthor = true;
|
|
44
|
+
if (width < PaneFold.FOLD_START_UNDER) {
|
|
45
|
+
if (!panes.foldStart && !this.keptOpenByAuthor) {
|
|
46
|
+
panes.foldStart = true;
|
|
47
|
+
this.foldedForWidth = true;
|
|
48
|
+
if (!foldSaid())
|
|
49
|
+
notify({ level: 'info', message: i18next.t('figure.text.parts-panel-folded-narrow') });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
if (this.foldedForWidth && panes.foldStart)
|
|
54
|
+
panes.foldStart = false;
|
|
55
|
+
this.foldedForWidth = false;
|
|
56
|
+
this.keptOpenByAuthor = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=pane-fold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pane-fold.js","sourceRoot":"","sources":["../../client/modeller/pane-fold.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAExC,mHAAmH;AACnH,SAAS,QAAQ;IACf,IAAI,CAAC;QACH,IAAI,YAAY,CAAC,OAAO,CAAC,kBAAkB,CAAC;YAAE,OAAO,IAAI,CAAA;QACzD,YAAY,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;EAKE;AACF,MAAM,OAAO,QAAQ;aACH,qBAAgB,GAAG,IAAI,AAAP,CAAO;IAKvC,4DAA4D;IAC5D,YACmB,IAA0C,EAC1C,KAAoC;QADpC,SAAI,GAAJ,IAAI,CAAsC;QAC1C,UAAK,GAAL,KAAK,CAA+B;QAP/C,mBAAc,GAAG,KAAK,CAAA;QACtB,qBAAgB,GAAG,KAAK,CAAA;QACxB,eAAU,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;QAOxF,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAED,aAAa;QACX,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpC,CAAC;IAED,gBAAgB;QACd,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,KAAa;QACjB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAA;QAC1B,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK;YAAE,OAAM;QAC5B,IAAI,IAAI,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,SAAS;YAAE,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAA;QACzE,IAAI,KAAK,GAAG,QAAQ,CAAC,gBAAgB,EAAE,CAAC;YACtC,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBAC/C,KAAK,CAAC,SAAS,GAAG,IAAI,CAAA;gBACtB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAA;gBAC1B,IAAI,CAAC,QAAQ,EAAE;oBAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,uCAAuC,CAAC,EAAE,CAAC,CAAA;YACzG,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,IAAI,CAAC,cAAc,IAAI,KAAK,CAAC,SAAS;gBAAE,KAAK,CAAC,SAAS,GAAG,KAAK,CAAA;YACnE,IAAI,CAAC,cAAc,GAAG,KAAK,CAAA;YAC3B,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAA;QAC/B,CAAC;IACH,CAAC","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport type { OxThreePane } from '@operato/layout'\nimport { notify } from '@operato/layout'\n\n/** Whether the narrow fold was said in this browser before; true from now on. Storage refused: said, as before. */\nfunction foldSaid(): boolean {\n try {\n if (localStorage.getItem('figure-fold-said')) return true\n localStorage.setItem('figure-fold-said', '1')\n } catch {\n /* No storage here: said every time, as it was. */\n }\n return false\n}\n\n/*\n Narrow, the parts list gives way to the canvas rather than all three shrinking (designer 2026-09-27, U11): under\n FOLD_START_UNDER px the canvas would be left under about 560 px beside both sides. The fold is said once, and the\n bar's own toggle opens it again; opened by the author, it is not folded again until the screen has been wide.\n Said once in this browser, not on every opening (2026-10-02): the list folds as before, the notice does not come back.\n*/\nexport class PaneFold implements ReactiveController {\n static readonly FOLD_START_UNDER = 1100\n private foldedForWidth = false\n private keptOpenByAuthor = false\n private widthWatch = new ResizeObserver(([entry]) => this.fitTo(entry!.contentRect.width))\n\n /** `panes` is the frame the bar folds, once it is drawn. */\n constructor(\n private readonly host: ReactiveControllerHost & HTMLElement,\n private readonly panes: () => OxThreePane | undefined\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.widthWatch.observe(this.host)\n }\n\n hostDisconnected() {\n this.widthWatch.disconnect()\n }\n\n fitTo(width: number) {\n const panes = this.panes()\n if (!panes || !width) return\n if (this.foldedForWidth && !panes.foldStart) this.keptOpenByAuthor = true\n if (width < PaneFold.FOLD_START_UNDER) {\n if (!panes.foldStart && !this.keptOpenByAuthor) {\n panes.foldStart = true\n this.foldedForWidth = true\n if (!foldSaid()) notify({ level: 'info', message: i18next.t('figure.text.parts-panel-folded-narrow') })\n }\n } else {\n if (this.foldedForWidth && panes.foldStart) panes.foldStart = false\n this.foldedForWidth = false\n this.keptOpenByAuthor = false\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class PanelMemory {
|
|
2
|
+
private readonly root;
|
|
3
|
+
private kept;
|
|
4
|
+
private restoreFor?;
|
|
5
|
+
/** `root` is where the properties panel is drawn: the editor's render root. */
|
|
6
|
+
constructor(root: () => ParentNode);
|
|
7
|
+
private panel;
|
|
8
|
+
private foldsOf;
|
|
9
|
+
/** Another model: nothing kept. */
|
|
10
|
+
clear(): void;
|
|
11
|
+
/** The selection moves from `was` to `now` ('' is the model): the panel as `was` left it is kept, `now`'s put back after the update. */
|
|
12
|
+
leave(was: string, now: string): void;
|
|
13
|
+
/** After the update: the panel of the part chosen as it was left, while that part is still the one chosen. */
|
|
14
|
+
restore(selected: () => string): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Per model and per part, how far its properties panel was scrolled and which of its folds were open, so going back
|
|
3
|
+
to one finds it as it was left (designer 2026-09-27, B.1). The path stays held at the top, so a scroll kept cannot
|
|
4
|
+
hide which part the panel speaks of. Another model starts clean.
|
|
5
|
+
*/
|
|
6
|
+
export class PanelMemory {
|
|
7
|
+
/** `root` is where the properties panel is drawn: the editor's render root. */
|
|
8
|
+
constructor(root) {
|
|
9
|
+
this.root = root;
|
|
10
|
+
this.kept = new Map();
|
|
11
|
+
}
|
|
12
|
+
panel() {
|
|
13
|
+
return this.root().querySelector('div[panel]');
|
|
14
|
+
}
|
|
15
|
+
foldsOf(panel) {
|
|
16
|
+
const folds = [...panel.querySelectorAll('details')];
|
|
17
|
+
for (const inner of panel.querySelectorAll('figure-links'))
|
|
18
|
+
folds.push(...(inner.shadowRoot?.querySelectorAll('details') ?? []));
|
|
19
|
+
return folds;
|
|
20
|
+
}
|
|
21
|
+
/** Another model: nothing kept. */
|
|
22
|
+
clear() {
|
|
23
|
+
this.kept.clear();
|
|
24
|
+
}
|
|
25
|
+
/** The selection moves from `was` to `now` ('' is the model): the panel as `was` left it is kept, `now`'s put back after the update. */
|
|
26
|
+
leave(was, now) {
|
|
27
|
+
const panel = this.panel();
|
|
28
|
+
if (panel)
|
|
29
|
+
this.kept.set(was || '@model', { top: panel.scrollTop, open: this.foldsOf(panel).map(d => d.open) });
|
|
30
|
+
this.restoreFor = now || '@model';
|
|
31
|
+
}
|
|
32
|
+
/** After the update: the panel of the part chosen as it was left, while that part is still the one chosen. */
|
|
33
|
+
restore(selected) {
|
|
34
|
+
if (this.restoreFor === undefined)
|
|
35
|
+
return;
|
|
36
|
+
const key = this.restoreFor;
|
|
37
|
+
this.restoreFor = undefined;
|
|
38
|
+
/*
|
|
39
|
+
Put back as soon as the links have drawn, before any click can come: the updates are microtasks. Put back a
|
|
40
|
+
frame later, it closed a fold opened by a click in that frame (2026-10-11, the row on a bench under load).
|
|
41
|
+
*/
|
|
42
|
+
const panel = this.panel();
|
|
43
|
+
if (panel)
|
|
44
|
+
void Promise.all([...panel.querySelectorAll('figure-links')].map(el => el.updateComplete)).then(() => {
|
|
45
|
+
if ((selected() || '@model') !== key)
|
|
46
|
+
return;
|
|
47
|
+
const kept = this.kept.get(key);
|
|
48
|
+
if (kept)
|
|
49
|
+
this.foldsOf(panel).forEach((fold, i) => i < kept.open.length && (fold.open = kept.open[i]));
|
|
50
|
+
panel.scrollTop = kept?.top ?? 0;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=panel-memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panel-memory.js","sourceRoot":"","sources":["../../client/modeller/panel-memory.ts"],"names":[],"mappings":"AAAA;;;;EAIE;AACF,MAAM,OAAO,WAAW;IAItB,+EAA+E;IAC/E,YAA6B,IAAsB;QAAtB,SAAI,GAAJ,IAAI,CAAkB;QAJ3C,SAAI,GAAG,IAAI,GAAG,EAA4C,CAAA;IAIZ,CAAC;IAE/C,KAAK;QACX,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,aAAa,CAAc,YAAY,CAAC,CAAA;IAC7D,CAAC;IAEO,OAAO,CAAC,KAAkB;QAChC,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAA;QACpD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,gBAAgB,CAAC,cAAc,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,gBAAgB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;QAChI,OAAO,KAAK,CAAA;IACd,CAAC;IAED,mCAAmC;IACnC,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAA;IACnB,CAAC;IAED,wIAAwI;IACxI,KAAK,CAAC,GAAW,EAAE,GAAW;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAA;QAC1B,IAAI,KAAK;YAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,QAAQ,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC/G,IAAI,CAAC,UAAU,GAAG,GAAG,IAAI,QAAQ,CAAA;IACnC,CAAC;IAED,8GAA8G;IAC9G,OAAO,CAAC,QAAsB;QAC5B,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;YAAE,OAAM;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAA;QAC3B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;QAC3B;;;UAGE;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAA;QAC1B,IAAI,KAAK;YACP,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,gBAAgB,CAAsD,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;gBACxJ,IAAI,CAAC,QAAQ,EAAE,IAAI,QAAQ,CAAC,KAAK,GAAG;oBAAE,OAAM;gBAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBAC/B,IAAI,IAAI;oBAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC,CAAA;gBACvG,KAAK,CAAC,SAAS,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAA;YAClC,CAAC,CAAC,CAAA;IACN,CAAC;CACF","sourcesContent":["/*\n Per model and per part, how far its properties panel was scrolled and which of its folds were open, so going back\n to one finds it as it was left (designer 2026-09-27, B.1). The path stays held at the top, so a scroll kept cannot\n hide which part the panel speaks of. Another model starts clean.\n*/\nexport class PanelMemory {\n private kept = new Map<string, { top: number; open: boolean[] }>()\n private restoreFor?: string\n\n /** `root` is where the properties panel is drawn: the editor's render root. */\n constructor(private readonly root: () => ParentNode) {}\n\n private panel() {\n return this.root().querySelector<HTMLElement>('div[panel]')\n }\n\n private foldsOf(panel: HTMLElement): HTMLDetailsElement[] {\n const folds = [...panel.querySelectorAll('details')]\n for (const inner of panel.querySelectorAll('figure-links')) folds.push(...(inner.shadowRoot?.querySelectorAll('details') ?? []))\n return folds\n }\n\n /** Another model: nothing kept. */\n clear() {\n this.kept.clear()\n }\n\n /** The selection moves from `was` to `now` ('' is the model): the panel as `was` left it is kept, `now`'s put back after the update. */\n leave(was: string, now: string) {\n const panel = this.panel()\n if (panel) this.kept.set(was || '@model', { top: panel.scrollTop, open: this.foldsOf(panel).map(d => d.open) })\n this.restoreFor = now || '@model'\n }\n\n /** After the update: the panel of the part chosen as it was left, while that part is still the one chosen. */\n restore(selected: () => string) {\n if (this.restoreFor === undefined) return\n const key = this.restoreFor\n this.restoreFor = undefined\n /*\n Put back as soon as the links have drawn, before any click can come: the updates are microtasks. Put back a\n frame later, it closed a fold opened by a click in that frame (2026-10-11, the row on a bench under load).\n */\n const panel = this.panel()\n if (panel)\n void Promise.all([...panel.querySelectorAll<HTMLElement & { updateComplete?: Promise<unknown> }>('figure-links')].map(el => el.updateComplete)).then(() => {\n if ((selected() || '@model') !== key) return\n const kept = this.kept.get(key)\n if (kept) this.foldsOf(panel).forEach((fold, i) => i < kept.open.length && (fold.open = kept.open[i]!))\n panel.scrollTop = kept?.top ?? 0\n })\n }\n}\n"]}
|
|
@@ -310,6 +310,8 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
310
310
|
palette: Record<string, string>;
|
|
311
311
|
};
|
|
312
312
|
edit(edit: FigurePartEdit): {
|
|
313
|
+
made?: string[] | undefined;
|
|
314
|
+
removal?: import("@hatiolab/figure-model").RemovalNotice | undefined;
|
|
313
315
|
context: null;
|
|
314
316
|
name: null;
|
|
315
317
|
fields: never[];
|
|
@@ -322,34 +324,8 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
322
324
|
palette?: undefined;
|
|
323
325
|
asset?: undefined;
|
|
324
326
|
} | {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
revision: number;
|
|
328
|
-
dirty: boolean;
|
|
329
|
-
canUndo: boolean;
|
|
330
|
-
canRedo: boolean;
|
|
331
|
-
fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
|
|
332
|
-
value: number;
|
|
333
|
-
}>;
|
|
334
|
-
readonly asset: import("@hatiolab/figure-model").FigureAsset;
|
|
335
|
-
geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
|
|
336
|
-
bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
|
|
337
|
-
palette: Record<string, string>;
|
|
338
|
-
} | {
|
|
339
|
-
removal: import("@hatiolab/figure-model").RemovalNotice;
|
|
340
|
-
context: null;
|
|
341
|
-
name: null;
|
|
342
|
-
fields: never[];
|
|
343
|
-
geometry: never[];
|
|
344
|
-
bindings: never[];
|
|
345
|
-
dirty: false;
|
|
346
|
-
revision: null;
|
|
347
|
-
canUndo?: undefined;
|
|
348
|
-
canRedo?: undefined;
|
|
349
|
-
palette?: undefined;
|
|
350
|
-
asset?: undefined;
|
|
351
|
-
} | {
|
|
352
|
-
removal: import("@hatiolab/figure-model").RemovalNotice;
|
|
327
|
+
made?: string[] | undefined;
|
|
328
|
+
removal?: import("@hatiolab/figure-model").RemovalNotice | undefined;
|
|
353
329
|
context: string;
|
|
354
330
|
name: string;
|
|
355
331
|
revision: number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PropertyValues } from 'lit';
|
|
2
|
+
import type { FigureAuthoringAction } from '@hatiolab/figure-model';
|
|
3
|
+
import type { FigureProductSession } from './product-session.js';
|
|
4
|
+
/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
|
|
5
|
+
export type Retry = {
|
|
6
|
+
label: string;
|
|
7
|
+
action?: FigureAuthoringAction;
|
|
8
|
+
run?: () => void;
|
|
9
|
+
};
|
|
10
|
+
/** What the teller reads of the editor, and what it asks of it. */
|
|
11
|
+
export interface RefusalHost {
|
|
12
|
+
session(): FigureProductSession | undefined;
|
|
13
|
+
error(): string;
|
|
14
|
+
notice(): string;
|
|
15
|
+
retry(): Retry | undefined;
|
|
16
|
+
/** Said as the editor's refusal. */
|
|
17
|
+
refuse(message: string): void;
|
|
18
|
+
/** A narrowed link, confirmed: run as the editor runs a command. */
|
|
19
|
+
command(action: FigureAuthoringAction): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
|
|
23
|
+
* notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
|
|
24
|
+
* delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
|
|
25
|
+
* other refusal is the app's notification, which stays until it is closed.
|
|
26
|
+
*/
|
|
27
|
+
export declare class RefusalTeller {
|
|
28
|
+
private readonly of;
|
|
29
|
+
private toldViewError;
|
|
30
|
+
constructor(of: RefusalHost);
|
|
31
|
+
/** After the editor's update: what changed in its refusal and notice, and the session's own error, said once. */
|
|
32
|
+
tell(changed: PropertyValues): void;
|
|
33
|
+
askClipLoss(error: unknown, again: () => void): boolean;
|
|
34
|
+
private confirmClipLoss;
|
|
35
|
+
private ask;
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { i18next } from '@operato/i18n';
|
|
2
|
+
import { notify } from '@operato/layout';
|
|
3
|
+
import { OxPrompt } from '@operato/popup';
|
|
4
|
+
import { clipLossOf } from './refusals.js';
|
|
5
|
+
/**
|
|
6
|
+
* Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
|
|
7
|
+
* notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
|
|
8
|
+
* delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
|
|
9
|
+
* other refusal is the app's notification, which stays until it is closed.
|
|
10
|
+
*/
|
|
11
|
+
export class RefusalTeller {
|
|
12
|
+
constructor(of) {
|
|
13
|
+
this.of = of;
|
|
14
|
+
this.toldViewError = '';
|
|
15
|
+
}
|
|
16
|
+
/** After the editor's update: what changed in its refusal and notice, and the session's own error, said once. */
|
|
17
|
+
tell(changed) {
|
|
18
|
+
const error = this.of.error();
|
|
19
|
+
if (changed.has('error') && error) {
|
|
20
|
+
const retry = this.of.retry();
|
|
21
|
+
if (retry)
|
|
22
|
+
this.ask(error, retry);
|
|
23
|
+
else
|
|
24
|
+
notify({ level: 'error', message: error });
|
|
25
|
+
}
|
|
26
|
+
const notice = this.of.notice();
|
|
27
|
+
if (changed.has('notice') && notice)
|
|
28
|
+
notify({ level: 'info', message: notice });
|
|
29
|
+
const viewError = this.of.session()?.read()?.error ?? '';
|
|
30
|
+
if (viewError && viewError !== this.toldViewError)
|
|
31
|
+
notify({ level: 'error', message: viewError });
|
|
32
|
+
this.toldViewError = viewError;
|
|
33
|
+
}
|
|
34
|
+
/*
|
|
35
|
+
An edit that would stop clips is refused by figure-model with the clips named (CLIPS_DROPPED). The person is told
|
|
36
|
+
which, and why, before anything is written; agreed, the same edit is made again letting exactly those go, as one
|
|
37
|
+
step undo brings back (designer 2026-09-27).
|
|
38
|
+
*/
|
|
39
|
+
askClipLoss(error, again) {
|
|
40
|
+
const clips = clipLossOf(error);
|
|
41
|
+
if (!clips)
|
|
42
|
+
return false;
|
|
43
|
+
/* The consent is for this edit, on this figure as it is now: held with it while the person reads. */
|
|
44
|
+
const session = this.of.session();
|
|
45
|
+
const { context, revision } = session.read();
|
|
46
|
+
void this.confirmClipLoss(clips, again, { session, context: context, revision: revision });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
async confirmClipLoss(clips, again, asked) {
|
|
50
|
+
const agreed = await OxPrompt.open({
|
|
51
|
+
type: 'warning',
|
|
52
|
+
text: i18next.t('figure.text.clips-would-go', { clips: clips.map(c => i18next.t(`figure.text.clip-${c.why}`, { clip: c.clip })).join(', ') }),
|
|
53
|
+
confirmButton: { text: i18next.t('figure.button.let-clips-go') },
|
|
54
|
+
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
55
|
+
});
|
|
56
|
+
if (!agreed)
|
|
57
|
+
return;
|
|
58
|
+
/* Changed while the person read -- another figure, or an edit in between: the consent was not for this; nothing is applied. */
|
|
59
|
+
const now = this.of.session() === asked.session ? asked.session.read() : undefined;
|
|
60
|
+
if (!now || now.context !== asked.context || now.revision !== asked.revision) {
|
|
61
|
+
this.of.refuse(i18next.t('figure.text.clips-consent-stale'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
asked.session.withConsent({ dropClips: clips.map(c => c.id), context: asked.context, revision: asked.revision }, again);
|
|
65
|
+
}
|
|
66
|
+
async ask(message, retry) {
|
|
67
|
+
const agreed = await OxPrompt.open({
|
|
68
|
+
type: 'question',
|
|
69
|
+
text: message,
|
|
70
|
+
confirmButton: { text: retry.label },
|
|
71
|
+
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
72
|
+
});
|
|
73
|
+
if (!agreed)
|
|
74
|
+
return;
|
|
75
|
+
if (retry.run)
|
|
76
|
+
retry.run();
|
|
77
|
+
else
|
|
78
|
+
this.of.command(retry.action);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=refusal-teller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusal-teller.js","sourceRoot":"","sources":["../../client/modeller/refusal-teller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PropertyValues } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { OxPrompt } from '@operato/popup'\nimport type { FigureAuthoringAction, FigureClipLoss } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\nimport { clipLossOf } from './refusals.js'\n\n/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */\nexport type Retry = { label: string; action?: FigureAuthoringAction; run?: () => void }\n\n/** What the teller reads of the editor, and what it asks of it. */\nexport interface RefusalHost {\n session(): FigureProductSession | undefined\n error(): string\n notice(): string\n retry(): Retry | undefined\n /** Said as the editor's refusal. */\n refuse(message: string): void\n /** A narrowed link, confirmed: run as the editor runs a command. */\n command(action: FigureAuthoringAction): void\n}\n\n/**\n * Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는\n * notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --\n * delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any\n * other refusal is the app's notification, which stays until it is closed.\n */\nexport class RefusalTeller {\n private toldViewError = ''\n\n constructor(private readonly of: RefusalHost) {}\n\n /** After the editor's update: what changed in its refusal and notice, and the session's own error, said once. */\n tell(changed: PropertyValues) {\n const error = this.of.error()\n if (changed.has('error') && error) {\n const retry = this.of.retry()\n if (retry) this.ask(error, retry)\n else notify({ level: 'error', message: error })\n }\n const notice = this.of.notice()\n if (changed.has('notice') && notice) notify({ level: 'info', message: notice })\n const viewError = this.of.session()?.read()?.error ?? ''\n if (viewError && viewError !== this.toldViewError) notify({ level: 'error', message: viewError })\n this.toldViewError = viewError\n }\n\n /*\n An edit that would stop clips is refused by figure-model with the clips named (CLIPS_DROPPED). The person is told\n which, and why, before anything is written; agreed, the same edit is made again letting exactly those go, as one\n step undo brings back (designer 2026-09-27).\n */\n askClipLoss(error: unknown, again: () => void): boolean {\n const clips = clipLossOf(error)\n if (!clips) return false\n /* The consent is for this edit, on this figure as it is now: held with it while the person reads. */\n const session = this.of.session()!\n const { context, revision } = session.read()\n void this.confirmClipLoss(clips, again, { session, context: context!, revision: revision! })\n return true\n }\n\n private async confirmClipLoss(clips: FigureClipLoss[], again: () => void, asked: { session: FigureProductSession; context: string; revision: number }) {\n const agreed = await OxPrompt.open({\n type: 'warning',\n text: i18next.t('figure.text.clips-would-go', { clips: clips.map(c => i18next.t(`figure.text.clip-${c.why}`, { clip: c.clip })).join(', ') }),\n confirmButton: { text: i18next.t('figure.button.let-clips-go') },\n cancelButton: { text: i18next.t('figure.button.keep-editing') }\n })\n if (!agreed) return\n /* Changed while the person read -- another figure, or an edit in between: the consent was not for this; nothing is applied. */\n const now = this.of.session() === asked.session ? asked.session.read() : undefined\n if (!now || now.context !== asked.context || now.revision !== asked.revision) {\n this.of.refuse(i18next.t('figure.text.clips-consent-stale'))\n return\n }\n asked.session.withConsent({ dropClips: clips.map(c => c.id), context: asked.context, revision: asked.revision }, again)\n }\n\n private async ask(message: string, retry: Retry) {\n const agreed = await OxPrompt.open({\n type: 'question',\n text: message,\n confirmButton: { text: retry.label },\n cancelButton: { text: i18next.t('figure.button.keep-editing') }\n })\n if (!agreed) return\n if (retry.run) retry.run()\n else this.of.command(retry.action!)\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FigureClipLoss, type FigureRefusalDetails } from '@hatiolab/figure-model';
|
|
1
2
|
type Say = (key: string, params: Record<string, string>) => string;
|
|
2
3
|
/** A size range the figure cannot be drawn over, or one that leaves out its own size, in words; nothing for any other refusal. */
|
|
3
4
|
export declare function sizeRangeRefusalOf(error: unknown, say: Say): string | null;
|
|
@@ -19,6 +20,15 @@ export declare function narrowingRetryOf<A extends {
|
|
|
19
20
|
* on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.
|
|
20
21
|
*/
|
|
21
22
|
export declare function placementRefusalOf(error: unknown, say: Say): string | null;
|
|
23
|
+
/** A refusal carried on in a record (an AI proposal's outcome) in figure-model's own shape: its code and details. */
|
|
24
|
+
export interface FigureRefusalRecord {
|
|
25
|
+
code?: string;
|
|
26
|
+
details?: FigureRefusalDetails;
|
|
27
|
+
}
|
|
28
|
+
/** The record of a refusal: figure-model's code and details, copied as they are. */
|
|
29
|
+
export declare function refusalRecordOf(error: unknown): FigureRefusalRecord;
|
|
30
|
+
/** The clips an edit would stop, as figure-model named them (CLIPS_DROPPED) -- the person is asked before any go; nothing for any other refusal. */
|
|
31
|
+
export declare function clipLossOf(error: unknown): FigureClipLoss[] | null;
|
|
22
32
|
/** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */
|
|
23
33
|
export declare function inDeclaredRoom(error: unknown): boolean;
|
|
24
34
|
/**
|
|
@@ -76,7 +86,8 @@ export declare function editRefusalOf(error: unknown, say: Say): string;
|
|
|
76
86
|
export declare function inputRangeRefusalOf(error: unknown, say: Say): string | null;
|
|
77
87
|
/**
|
|
78
88
|
* A row past its limits: a pitch so fine it would hold more copies than a row takes, a row that would expand to more
|
|
79
|
-
* bodies than a figure draws,
|
|
89
|
+
* bodies than a figure draws, a number of copies outside what a row may have or more than its length holds apart, a
|
|
90
|
+
* row inside a row along the same axis, or a part whose face something holds made into a row.
|
|
80
91
|
*/
|
|
81
92
|
export declare function rowLimitRefusalOf(error: unknown, say: Say): string | null;
|
|
82
93
|
/** A row's pitch under which the figure, at the length it is authored, would hold more copies than a row takes. */
|
|
@@ -70,6 +70,15 @@ export function placementRefusalOf(error, say) {
|
|
|
70
70
|
faces: misfit.takes.join(', ')
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
/** The record of a refusal: figure-model's code and details, copied as they are. */
|
|
74
|
+
export function refusalRecordOf(error) {
|
|
75
|
+
const { details } = (error ?? {});
|
|
76
|
+
return { code: codeOf(error), ...(details ? { details } : {}) };
|
|
77
|
+
}
|
|
78
|
+
/** The clips an edit would stop, as figure-model named them (CLIPS_DROPPED) -- the person is asked before any go; nothing for any other refusal. */
|
|
79
|
+
export function clipLossOf(error) {
|
|
80
|
+
return codeOf(error) === 'CLIPS_DROPPED' ? (detailsOf(error, 'clips-dropped')?.clips ?? []) : null;
|
|
81
|
+
}
|
|
73
82
|
/** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */
|
|
74
83
|
export function inDeclaredRoom(error) {
|
|
75
84
|
return codeOf(error) === 'PART_REFERENCED' && !!detailsOf(error, 'in-declared-space');
|
|
@@ -105,6 +114,10 @@ export function placedRefusalOf(error, part, say) {
|
|
|
105
114
|
}
|
|
106
115
|
/** A part name refused: not a name, already used, or another part is named under the part. The rule is the kernel's. */
|
|
107
116
|
export function partNameRefusalOf(error, say) {
|
|
117
|
+
/* A new part named as one already is: the same sentence as a part renamed so. */
|
|
118
|
+
const takenId = detailsOf(error, 'part-id-taken');
|
|
119
|
+
if (takenId)
|
|
120
|
+
return say('figure.text.part-name-taken', { name: takenId.part });
|
|
108
121
|
if (codeOf(error) !== 'PART_ID')
|
|
109
122
|
return null;
|
|
110
123
|
const invalid = detailsOf(error, 'name-invalid');
|
|
@@ -249,7 +262,8 @@ export function inputRangeRefusalOf(error, say) {
|
|
|
249
262
|
}
|
|
250
263
|
/**
|
|
251
264
|
* A row past its limits: a pitch so fine it would hold more copies than a row takes, a row that would expand to more
|
|
252
|
-
* bodies than a figure draws,
|
|
265
|
+
* bodies than a figure draws, a number of copies outside what a row may have or more than its length holds apart, a
|
|
266
|
+
* row inside a row along the same axis, or a part whose face something holds made into a row.
|
|
253
267
|
*/
|
|
254
268
|
export function rowLimitRefusalOf(error, say) {
|
|
255
269
|
const copies = detailsOf(error, 'copies-over-limit');
|
|
@@ -261,8 +275,19 @@ export function rowLimitRefusalOf(error, say) {
|
|
|
261
275
|
const count = detailsOf(error, 'row-copies-out-of-range');
|
|
262
276
|
if (count)
|
|
263
277
|
return say('figure.text.row-copies-out-of-range', { count: count.count.toLocaleString(), min: String(count.min), max: String(count.max) });
|
|
278
|
+
const fit = detailsOf(error, 'row-does-not-fit');
|
|
279
|
+
if (fit)
|
|
280
|
+
return say('figure.text.row-does-not-fit', { count: fit.count.toLocaleString(), length: mmOf(fit.length), extent: mmOf(fit.extent) });
|
|
281
|
+
const axis = detailsOf(error, 'row-same-axis');
|
|
282
|
+
if (axis)
|
|
283
|
+
return say('figure.text.row-same-axis', { part: axis.part, outer: axis.outer, axis: axis.axis.toUpperCase() });
|
|
284
|
+
const held = detailsOf(error, 'surface-on-row');
|
|
285
|
+
if (held)
|
|
286
|
+
return say(`figure.text.row-face-held-${heldByOf(held.at)}`, { part: held.part });
|
|
264
287
|
return null;
|
|
265
288
|
}
|
|
289
|
+
/* What holds a face, from the contract path the kernel names it by: the slot layout, a place, the footprint. */
|
|
290
|
+
const heldByOf = (at) => (at.startsWith('slotLayout') ? 'slot-layout' : at.startsWith('bindings') ? 'place' : at.startsWith('occupancy') ? 'footprint' : 'other');
|
|
266
291
|
/** A row's pitch under which the figure, at the length it is authored, would hold more copies than a row takes. */
|
|
267
292
|
export function repeatPitchRefusalOf(error, say) {
|
|
268
293
|
if (codeOf(error) !== 'INPUT_RANGE')
|