@things-factory/figure-ui 10.1.152 → 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 +58 -619
- package/client/modeller/figure-first-save.ts +9 -1
- 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 +18 -1
- 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 -50
- package/dist-client/modeller/editor.js +69 -641
- package/dist-client/modeller/editor.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/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/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 +10 -0
- package/dist-client/modeller/refusals.js +9 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/browser/fix-alignment.check.mjs +13 -2
- package/test/browser/links.check.mjs +36 -9
- package/test/browser/modeller.check.mjs +25 -0
- package/test/browser/parts-page.js +9 -1
- package/test/browser/parts.check.mjs +22 -0
- package/test/browser/places-mesh.check.mjs +25 -9
- package/test/browser/product-page.check.mjs +42 -2
- package/test/browser/screen.check.mjs +33 -0
- package/test/clip-ai-proposal.test.ts +4 -1
- package/test/clip-consent.test.ts +9 -2
- package/test/layout.test.ts +3 -1
- package/test/refusals.test.ts +18 -10
- package/test/theme.test.ts +5 -15
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { LitElement, html,
|
|
1
|
+
import { LitElement, html, nothing, type PropertyValues } from 'lit'
|
|
2
2
|
import { i18next } from '@operato/i18n'
|
|
3
3
|
import type { OxThreePane } from '@operato/layout'
|
|
4
4
|
import '@operato/layout'
|
|
5
|
-
import {
|
|
5
|
+
import { openPopup } from '@operato/layout'
|
|
6
6
|
import { OxPrompt } from '@operato/popup'
|
|
7
7
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
8
8
|
import { live } from 'lit/directives/live.js'
|
|
9
|
-
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact,
|
|
9
|
+
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact, 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
12
|
import type { FigureAsset, FigureAuthoringAction, FigureEvaluatedShape, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement, FigureFaceMappingNeed, FigureFaceRemap } from '@hatiolab/figure-model'
|
|
@@ -18,7 +18,8 @@ import './figure-parts.js'
|
|
|
18
18
|
import './figure-material-panel.js'
|
|
19
19
|
import './figure-section-panel.js'
|
|
20
20
|
import './figure-section-editor.js'
|
|
21
|
-
import {
|
|
21
|
+
import { rowWordsOf } from './command-preview.js'
|
|
22
|
+
import { CommandPreview, NO_WORDS } from './command-preview-controller.js'
|
|
22
23
|
import { motionEndsOf, type MotionEnds } from './motion-ends.js'
|
|
23
24
|
import './figure-links.js'
|
|
24
25
|
import './figure-controls.js'
|
|
@@ -28,9 +29,14 @@ import './figure-settings.js'
|
|
|
28
29
|
import { DEFAULT_VIEW, type ViewSettings } from './figure-view.js'
|
|
29
30
|
import './figure-capability-panel.js'
|
|
30
31
|
import { gizmoChanges, gizmoReachOf, type GizmoReach, type GizmoResult } from './gizmo.js'
|
|
31
|
-
import {
|
|
32
|
+
import type { KeyAction } from './keys.js'
|
|
33
|
+
import { ModellerKeys } from './modeller-keys.js'
|
|
34
|
+
import { PaneFold } from './pane-fold.js'
|
|
35
|
+
import { PanelMemory } from './panel-memory.js'
|
|
36
|
+
import { RefusalTeller, type Retry } from './refusal-teller.js'
|
|
32
37
|
import { groupFields, ownerOf, partLabelOf, repeatOf, type FieldLike, type FieldGroup } from './fields.js'
|
|
33
38
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
39
|
+
import { EditorStyles } from './editor-styles.js'
|
|
34
40
|
import { ScrollbarStyles } from '@operato/styles'
|
|
35
41
|
import { alignChanges, canMirror, mirrorChanges, type AlignTool } from './align.js'
|
|
36
42
|
import type { FigurePaletteKind, FigurePartRow } from './figure-parts.js'
|
|
@@ -54,7 +60,7 @@ import type { FigureRenderPart } from './renderer.js'
|
|
|
54
60
|
import type { FigureBudgetLine } from './maturity.js'
|
|
55
61
|
import { endPlaceKeyOf, fieldWordsOf, followOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
|
|
56
62
|
import { jointFanOf } from './joint-fan.js'
|
|
57
|
-
import {
|
|
63
|
+
import { derivedOf, type FigureRevision } from './revision.js'
|
|
58
64
|
import { jointDragOf } from './joint-drag.js'
|
|
59
65
|
import type { FirstSave } from './first-save.js'
|
|
60
66
|
import { isLive, livePartsOf, liveOverSession, NOTHING_LIVE, scrubbed, scrubStep, type LiveValues } from './live.js'
|
|
@@ -67,332 +73,13 @@ import { startingShapeKindOfBody } from './starting-shape.js'
|
|
|
67
73
|
*/
|
|
68
74
|
type SideTab = 'properties' | 'capability' | 'motion' | 'verdict' | 'view' | 'history'
|
|
69
75
|
|
|
70
|
-
/** Whether the narrow fold was said in this browser before; true from now on. Storage refused: said, as before. */
|
|
71
|
-
function foldSaid(): boolean {
|
|
72
|
-
try {
|
|
73
|
-
if (localStorage.getItem('figure-fold-said')) return true
|
|
74
|
-
localStorage.setItem('figure-fold-said', '1')
|
|
75
|
-
} catch {
|
|
76
|
-
/* No storage here: said every time, as it was. */
|
|
77
|
-
}
|
|
78
|
-
return false
|
|
79
|
-
}
|
|
80
|
-
|
|
81
76
|
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
82
|
-
/** The words a panel is given when there are none: one array, so it does not read as a change. */
|
|
83
|
-
const NO_WORDS: string[] = []
|
|
84
77
|
const NO_OVERHANG = { words: NO_WORDS, note: NO_WORDS }
|
|
85
78
|
|
|
86
|
-
/** How long a command's candidate rests before its preview is worked out. */
|
|
87
|
-
const PREVIEW_SETTLE_MS = 150
|
|
88
|
-
|
|
89
79
|
@customElement('figure-editor')
|
|
90
80
|
export class FigureEditor extends LitElement {
|
|
91
81
|
/* The operato scrollbar, as every scrolling panel of the app has it -- the side panel scrolled with the browser's own. */
|
|
92
|
-
static styles = [ScrollbarStyles, SidePanelStyles,
|
|
93
|
-
:host {
|
|
94
|
-
display: flex;
|
|
95
|
-
flex-direction: column;
|
|
96
|
-
min-height: 0;
|
|
97
|
-
height: 100%;
|
|
98
|
-
/* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */
|
|
99
|
-
container-type: inline-size;
|
|
100
|
-
font: 13px sans-serif;
|
|
101
|
-
color: var(--md-sys-color-on-surface);
|
|
102
|
-
}
|
|
103
|
-
/* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and
|
|
104
|
-
fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */
|
|
105
|
-
ox-canvas-bar button[mode] {
|
|
106
|
-
height: 28px;
|
|
107
|
-
padding: 0 10px;
|
|
108
|
-
border-radius: 999em;
|
|
109
|
-
border-color: transparent;
|
|
110
|
-
background: none;
|
|
111
|
-
}
|
|
112
|
-
ox-canvas-bar button[mode][aria-pressed='true'] {
|
|
113
|
-
border-color: var(--md-sys-color-primary);
|
|
114
|
-
}
|
|
115
|
-
/* The gizmo's tools, drawn as the bar's own icon buttons (undo, redo, fit). */
|
|
116
|
-
ox-canvas-bar .gizmo-tools {
|
|
117
|
-
display: inline-flex;
|
|
118
|
-
gap: 2px;
|
|
119
|
-
padding-left: 4px;
|
|
120
|
-
border-left: 1px solid var(--md-sys-color-outline-variant);
|
|
121
|
-
}
|
|
122
|
-
ox-canvas-bar button[tool] {
|
|
123
|
-
display: inline-grid;
|
|
124
|
-
place-items: center;
|
|
125
|
-
width: 32px;
|
|
126
|
-
height: 32px;
|
|
127
|
-
padding: 0;
|
|
128
|
-
border: none;
|
|
129
|
-
border-radius: var(--md-sys-shape-corner-small, 8px);
|
|
130
|
-
background: transparent;
|
|
131
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
132
|
-
cursor: pointer;
|
|
133
|
-
}
|
|
134
|
-
ox-canvas-bar button[tool]:hover:not(:disabled) {
|
|
135
|
-
background: var(--md-sys-color-surface-container-high);
|
|
136
|
-
color: var(--md-sys-color-on-surface);
|
|
137
|
-
}
|
|
138
|
-
ox-canvas-bar button[tool][aria-pressed='true'] {
|
|
139
|
-
background: var(--md-sys-color-secondary-container);
|
|
140
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
141
|
-
}
|
|
142
|
-
ox-canvas-bar button[tool]:disabled {
|
|
143
|
-
opacity: 0.38;
|
|
144
|
-
cursor: default;
|
|
145
|
-
}
|
|
146
|
-
/*
|
|
147
|
-
Slotted into the bar from here, the tools do not get the bar's own icon size (20px, set inside its shadow): they
|
|
148
|
-
showed at md-icon's 24px, heavier than undo, redo and fit beside them. The same size, and the bar's focus ring.
|
|
149
|
-
*/
|
|
150
|
-
ox-canvas-bar button[tool] md-icon {
|
|
151
|
-
--md-icon-size: 20px;
|
|
152
|
-
}
|
|
153
|
-
ox-canvas-bar button[tool]:focus-visible {
|
|
154
|
-
outline: 2px solid var(--md-sys-color-primary);
|
|
155
|
-
outline-offset: 1px;
|
|
156
|
-
}
|
|
157
|
-
figure-preview {
|
|
158
|
-
min-width: 0;
|
|
159
|
-
min-height: 0;
|
|
160
|
-
}
|
|
161
|
-
/* The edit canvas stays standing while the preview is in front (a 3D scene costs a third of a second to make), out of sight. */
|
|
162
|
-
div[middle][hidden] {
|
|
163
|
-
display: none;
|
|
164
|
-
}
|
|
165
|
-
/* The preview, once made, stays standing out of sight too: its 3D scene is not made again each time it is asked for. */
|
|
166
|
-
figure-preview[hidden] {
|
|
167
|
-
display: none;
|
|
168
|
-
}
|
|
169
|
-
ox-canvas-bar .status {
|
|
170
|
-
padding: 0 8px;
|
|
171
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
172
|
-
}
|
|
173
|
-
ox-canvas-bar .status[dirty] {
|
|
174
|
-
color: var(--md-sys-color-primary);
|
|
175
|
-
font-weight: 600;
|
|
176
|
-
}
|
|
177
|
-
ox-canvas-bar button[save] {
|
|
178
|
-
height: 32px;
|
|
179
|
-
padding: 0 14px;
|
|
180
|
-
border: none;
|
|
181
|
-
border-radius: 999em;
|
|
182
|
-
color: var(--md-sys-color-on-primary);
|
|
183
|
-
background: var(--md-sys-color-primary);
|
|
184
|
-
}
|
|
185
|
-
/* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under
|
|
186
|
-
860px and one column with the middle first under 560px are the frame's, as are the dividers. */
|
|
187
|
-
ox-three-pane {
|
|
188
|
-
flex: 1;
|
|
189
|
-
min-height: 0;
|
|
190
|
-
}
|
|
191
|
-
figure-parts {
|
|
192
|
-
min-height: 0;
|
|
193
|
-
}
|
|
194
|
-
figure-canvas {
|
|
195
|
-
flex: 1;
|
|
196
|
-
min-width: 0;
|
|
197
|
-
min-height: 0;
|
|
198
|
-
}
|
|
199
|
-
div[middle] {
|
|
200
|
-
position: relative;
|
|
201
|
-
display: flex;
|
|
202
|
-
min-width: 0;
|
|
203
|
-
min-height: 0;
|
|
204
|
-
}
|
|
205
|
-
/* A new figure's first question, over the empty canvas. */
|
|
206
|
-
div[start] {
|
|
207
|
-
position: absolute;
|
|
208
|
-
inset: 50% auto auto 50%;
|
|
209
|
-
z-index: 10;
|
|
210
|
-
color: var(--md-sys-color-on-surface);
|
|
211
|
-
transform: translate(-50%, -50%);
|
|
212
|
-
width: min(440px, calc(100% - 32px));
|
|
213
|
-
box-sizing: border-box;
|
|
214
|
-
padding: 20px;
|
|
215
|
-
border-radius: 12px;
|
|
216
|
-
background: var(--md-sys-color-surface, #fff);
|
|
217
|
-
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
|
|
218
|
-
}
|
|
219
|
-
div[start] button[start-button] {
|
|
220
|
-
width: auto;
|
|
221
|
-
margin: 0;
|
|
222
|
-
font-size: var(--ui-t-body);
|
|
223
|
-
height: 36px;
|
|
224
|
-
padding: 0 16px;
|
|
225
|
-
border: 1px solid var(--md-sys-color-outline);
|
|
226
|
-
border-radius: 999em;
|
|
227
|
-
color: var(--md-sys-color-primary);
|
|
228
|
-
background: var(--md-sys-color-surface, #fff);
|
|
229
|
-
cursor: pointer;
|
|
230
|
-
}
|
|
231
|
-
div[start] button[start-button]:hover:not(:disabled) {
|
|
232
|
-
background: var(--md-sys-color-primary-container);
|
|
233
|
-
}
|
|
234
|
-
div[start] button[start-button][pressed] {
|
|
235
|
-
color: var(--md-sys-color-on-primary);
|
|
236
|
-
background: var(--md-sys-color-primary);
|
|
237
|
-
border-color: var(--md-sys-color-primary);
|
|
238
|
-
}
|
|
239
|
-
div[start] button[start-button]:disabled {
|
|
240
|
-
opacity: 0.5;
|
|
241
|
-
cursor: default;
|
|
242
|
-
}
|
|
243
|
-
div[start] h2 {
|
|
244
|
-
margin: 0 0 12px;
|
|
245
|
-
font-size: var(--ui-t-title);
|
|
246
|
-
}
|
|
247
|
-
div[start-paths] {
|
|
248
|
-
display: grid;
|
|
249
|
-
gap: 14px;
|
|
250
|
-
}
|
|
251
|
-
div[start-paths] h3 {
|
|
252
|
-
margin: 0 0 6px;
|
|
253
|
-
font-size: var(--ui-t-body);
|
|
254
|
-
}
|
|
255
|
-
div[start-shapes] {
|
|
256
|
-
display: flex;
|
|
257
|
-
gap: 8px;
|
|
258
|
-
flex-wrap: wrap;
|
|
259
|
-
}
|
|
260
|
-
div[start] small {
|
|
261
|
-
display: block;
|
|
262
|
-
margin-top: 4px;
|
|
263
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
264
|
-
}
|
|
265
|
-
/* The side: tabs over one panel at a time, each using the whole height. */
|
|
266
|
-
aside {
|
|
267
|
-
display: flex;
|
|
268
|
-
flex-direction: column;
|
|
269
|
-
min-height: 0;
|
|
270
|
-
background: var(--md-sys-color-surface-container-lowest);
|
|
271
|
-
}
|
|
272
|
-
div[tabs] {
|
|
273
|
-
display: flex;
|
|
274
|
-
flex: none;
|
|
275
|
-
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
276
|
-
}
|
|
277
|
-
button[tab] {
|
|
278
|
-
flex: 1;
|
|
279
|
-
position: relative;
|
|
280
|
-
display: flex;
|
|
281
|
-
flex-direction: column;
|
|
282
|
-
align-items: center;
|
|
283
|
-
justify-content: center;
|
|
284
|
-
gap: 1px;
|
|
285
|
-
min-width: 0;
|
|
286
|
-
height: 48px;
|
|
287
|
-
padding: 0;
|
|
288
|
-
border: none;
|
|
289
|
-
border-radius: 0;
|
|
290
|
-
border-bottom: 2px solid transparent;
|
|
291
|
-
background: none;
|
|
292
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
293
|
-
}
|
|
294
|
-
button[tab]:hover:not([on]) {
|
|
295
|
-
background: var(--md-sys-color-surface-container);
|
|
296
|
-
}
|
|
297
|
-
button[tab][on] {
|
|
298
|
-
color: var(--md-sys-color-primary);
|
|
299
|
-
border-bottom-color: var(--md-sys-color-primary);
|
|
300
|
-
}
|
|
301
|
-
button[tab] md-icon {
|
|
302
|
-
font-size: 20px;
|
|
303
|
-
width: 20px;
|
|
304
|
-
height: 20px;
|
|
305
|
-
}
|
|
306
|
-
button[tab] span[tab-label] {
|
|
307
|
-
max-width: 100%;
|
|
308
|
-
overflow: hidden;
|
|
309
|
-
font-size: 0.66rem;
|
|
310
|
-
line-height: 1.2;
|
|
311
|
-
white-space: nowrap;
|
|
312
|
-
text-overflow: ellipsis;
|
|
313
|
-
}
|
|
314
|
-
span[badge] {
|
|
315
|
-
position: absolute;
|
|
316
|
-
top: 3px;
|
|
317
|
-
right: 2px;
|
|
318
|
-
padding: 0 5px;
|
|
319
|
-
font-size: 0.62rem;
|
|
320
|
-
font-weight: 700;
|
|
321
|
-
line-height: 1.5;
|
|
322
|
-
color: var(--md-sys-color-on-primary-container);
|
|
323
|
-
background: var(--md-sys-color-primary-container);
|
|
324
|
-
border-radius: 999em;
|
|
325
|
-
pointer-events: none;
|
|
326
|
-
}
|
|
327
|
-
/* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */
|
|
328
|
-
div[panel] {
|
|
329
|
-
flex: 1;
|
|
330
|
-
min-height: 0;
|
|
331
|
-
overflow: auto;
|
|
332
|
-
padding: 0;
|
|
333
|
-
}
|
|
334
|
-
div[panel][flush] {
|
|
335
|
-
padding: 0;
|
|
336
|
-
}
|
|
337
|
-
/* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */
|
|
338
|
-
[off] {
|
|
339
|
-
display: none !important;
|
|
340
|
-
}
|
|
341
|
-
/* Which part the panel speaks of. */
|
|
342
|
-
div[subject] {
|
|
343
|
-
position: sticky;
|
|
344
|
-
top: 0;
|
|
345
|
-
z-index: 1;
|
|
346
|
-
display: grid;
|
|
347
|
-
gap: 4px;
|
|
348
|
-
padding: 10px 12px;
|
|
349
|
-
background: var(--md-sys-color-surface-container-lowest);
|
|
350
|
-
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
351
|
-
}
|
|
352
|
-
/* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */
|
|
353
|
-
div[subject-summary] {
|
|
354
|
-
display: grid;
|
|
355
|
-
gap: 2px;
|
|
356
|
-
}
|
|
357
|
-
div[subject-summary] b {
|
|
358
|
-
font-size: var(--ui-t-body, 13px);
|
|
359
|
-
font-weight: 600;
|
|
360
|
-
}
|
|
361
|
-
div[subject-summary] b span {
|
|
362
|
-
font-weight: 400;
|
|
363
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
364
|
-
}
|
|
365
|
-
div[subject-summary] small {
|
|
366
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
367
|
-
}
|
|
368
|
-
button {
|
|
369
|
-
font: inherit;
|
|
370
|
-
box-sizing: border-box;
|
|
371
|
-
height: 32px;
|
|
372
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
373
|
-
border-radius: 6px;
|
|
374
|
-
background: var(--md-sys-color-surface);
|
|
375
|
-
color: inherit;
|
|
376
|
-
padding: 0 8px;
|
|
377
|
-
min-width: 0;
|
|
378
|
-
}
|
|
379
|
-
button {
|
|
380
|
-
cursor: pointer;
|
|
381
|
-
}
|
|
382
|
-
button:disabled {
|
|
383
|
-
opacity: 0.45;
|
|
384
|
-
cursor: default;
|
|
385
|
-
}
|
|
386
|
-
button[aria-pressed='true'] {
|
|
387
|
-
background: var(--md-sys-color-secondary-container);
|
|
388
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
389
|
-
border-color: var(--md-sys-color-primary);
|
|
390
|
-
}
|
|
391
|
-
.hint {
|
|
392
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
393
|
-
line-height: 1.5;
|
|
394
|
-
}
|
|
395
|
-
`]
|
|
82
|
+
static styles = [ScrollbarStyles, SidePanelStyles, EditorStyles]
|
|
396
83
|
@property({ attribute: false }) session?: FigureProductSession
|
|
397
84
|
/** The saved figure's state and version, as the server gave them (the page reads the row). */
|
|
398
85
|
@property({ attribute: false }) releaseState?: { state?: 'draft' | 'released'; version: number }
|
|
@@ -425,7 +112,7 @@ export class FigureEditor extends LitElement {
|
|
|
425
112
|
*/
|
|
426
113
|
@state() private faceMapping?: { action: FigureAuthoringAction; need: FigureFaceMappingNeed; from: 'section-editor' | 'panel' }
|
|
427
114
|
/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
|
|
428
|
-
@state() private retry?:
|
|
115
|
+
@state() private retry?: Retry
|
|
429
116
|
@state() private tab: SideTab = 'properties'
|
|
430
117
|
/** The frame the bar folds - found after the first render, since the bar is drawn before it. */
|
|
431
118
|
@state() private panes?: OxThreePane
|
|
@@ -467,13 +154,14 @@ export class FigureEditor extends LitElement {
|
|
|
467
154
|
@state() private live: LiveValues = NOTHING_LIVE
|
|
468
155
|
/** The part whose drawn section is open in the section editor (ADR-0100 §3.2), or null. */
|
|
469
156
|
@state() private sectionEditing: string | null = null
|
|
470
|
-
/**
|
|
471
|
-
* A command seen before it is done (ADR-0097 §5.5): worked out on a copy for the figure, version, part and command it
|
|
472
|
-
* names, and shown only while all four are still the ones on screen. Nothing of it is saved or undone.
|
|
473
|
-
*/
|
|
474
|
-
@state() private preview?: HeldPreview & { part: string }
|
|
475
|
-
private previewDrawn?: { of: HeldPreview; ghost: FigureRenderPart[]; highlight: FigureRenderPart | null }
|
|
476
157
|
private lastContext: string | null = null
|
|
158
|
+
private commandPreview = new CommandPreview(this, {
|
|
159
|
+
session: () => this.session,
|
|
160
|
+
selected: () => this.selected,
|
|
161
|
+
shown: () => this.shown(),
|
|
162
|
+
formsInSight: () => this.middle === 'edit' && this.tab === 'properties',
|
|
163
|
+
said: error => this.said(error)
|
|
164
|
+
})
|
|
477
165
|
/**
|
|
478
166
|
* Move, turn and size on the canvas, in the bar's tools: the gizmo the W · E · R keys give, found without knowing the
|
|
479
167
|
* keys (2026-09-26 -- a person dragging the part turned the camera). Each is offered only where the part's own
|
|
@@ -501,29 +189,22 @@ export class FigureEditor extends LitElement {
|
|
|
501
189
|
)}
|
|
502
190
|
</span>`
|
|
503
191
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
192
|
+
private keys = new ModellerKeys(this, {
|
|
193
|
+
taking: () => {
|
|
194
|
+
const view = this.session?.read()
|
|
195
|
+
/*
|
|
196
|
+
* The section editor open takes the keys for its own: Esc cancels it, Delete deletes a point, Ctrl+Z undoes a point.
|
|
197
|
+
* Heard here first, they deselected the part, deleted the part and undid the figure's last step (2026-09-30).
|
|
198
|
+
*/
|
|
199
|
+
return !!view && !view.loading && !view.saving && !!view.context && this.isConnected && !this.sectionEditing
|
|
200
|
+
},
|
|
201
|
+
scene: () => this.renderRoot.querySelector<FigureCanvas>('figure-canvas'),
|
|
202
|
+
selected: () => this.selected,
|
|
203
|
+
act: action => this.keyAction(action)
|
|
204
|
+
})
|
|
205
|
+
private keyAction(action: KeyAction) {
|
|
206
|
+
const view = this.session!.read()
|
|
513
207
|
const canvas = this.renderRoot.querySelector<FigureCanvas>('figure-canvas')
|
|
514
|
-
const path = e.composedPath()
|
|
515
|
-
const action = keyActionOf(
|
|
516
|
-
{ key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey, altKey: e.altKey, target: path[0] as HTMLElement, inScene: !!canvas && path.includes(canvas) },
|
|
517
|
-
this.selected
|
|
518
|
-
)
|
|
519
|
-
if (!action) return
|
|
520
|
-
e.preventDefault()
|
|
521
|
-
/*
|
|
522
|
-
The session's history is the only one. With the canvas focused the scene took Ctrl+Z as well, undoing on its own
|
|
523
|
-
board the last value the canvas had written -- which the canvas then told as a drag: an undo of a drag on a
|
|
524
|
-
part placed by a rule wrote the drag again (2026-09-25). Heard first (capture) and kept from the scene.
|
|
525
|
-
*/
|
|
526
|
-
e.stopPropagation()
|
|
527
208
|
if (action === 'translate' || action === 'rotate' || action === 'scale') canvas && this.useGizmo(action)
|
|
528
209
|
else if (action === 'undo') view.canUndo && this.run(() => this.session!.undo())
|
|
529
210
|
else if (action === 'redo') view.canRedo && this.run(() => this.session!.redo())
|
|
@@ -531,118 +212,16 @@ export class FigureEditor extends LitElement {
|
|
|
531
212
|
else if (action === 'remove') this.removePart(this.selected)
|
|
532
213
|
else this.selected = ''
|
|
533
214
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
this.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
this.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
super.disconnectedCallback()
|
|
545
|
-
}
|
|
546
|
-
/*
|
|
547
|
-
Narrow, the parts list gives way to the canvas rather than all three shrinking (designer 2026-09-27, U11): under
|
|
548
|
-
FOLD_START_UNDER px the canvas would be left under about 560 px beside both sides. The fold is said once, and the
|
|
549
|
-
bar's own toggle opens it again; opened by the author, it is not folded again until the screen has been wide.
|
|
550
|
-
Said once in this browser, not on every opening (2026-10-02): the list folds as before, the notice does not come back.
|
|
551
|
-
*/
|
|
552
|
-
private static readonly FOLD_START_UNDER = 1100
|
|
553
|
-
private foldedForWidth = false
|
|
554
|
-
private keptOpenByAuthor = false
|
|
555
|
-
private widthWatch = new ResizeObserver(([entry]) => this.fitPanesTo(entry!.contentRect.width))
|
|
556
|
-
private fitPanesTo(width: number) {
|
|
557
|
-
const panes = this.panes
|
|
558
|
-
if (!panes || !width) return
|
|
559
|
-
if (this.foldedForWidth && !panes.foldStart) this.keptOpenByAuthor = true
|
|
560
|
-
if (width < FigureEditor.FOLD_START_UNDER) {
|
|
561
|
-
if (!panes.foldStart && !this.keptOpenByAuthor) {
|
|
562
|
-
panes.foldStart = true
|
|
563
|
-
this.foldedForWidth = true
|
|
564
|
-
if (!foldSaid()) notify({ level: 'info', message: i18next.t('figure.text.parts-panel-folded-narrow') })
|
|
565
|
-
}
|
|
566
|
-
} else {
|
|
567
|
-
if (this.foldedForWidth && panes.foldStart) panes.foldStart = false
|
|
568
|
-
this.foldedForWidth = false
|
|
569
|
-
this.keptOpenByAuthor = false
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
|
|
574
|
-
* notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
|
|
575
|
-
* delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
|
|
576
|
-
* other refusal is the app's notification, which stays until it is closed.
|
|
577
|
-
*/
|
|
578
|
-
private toldViewError = ''
|
|
579
|
-
private tell(changed: PropertyValues) {
|
|
580
|
-
if (changed.has('error') && this.error) {
|
|
581
|
-
const retry = this.retry
|
|
582
|
-
if (retry) this.ask(this.error, retry)
|
|
583
|
-
else notify({ level: 'error', message: this.error })
|
|
584
|
-
}
|
|
585
|
-
if (changed.has('notice') && this.notice) notify({ level: 'info', message: this.notice })
|
|
586
|
-
const viewError = this.session?.read()?.error ?? ''
|
|
587
|
-
if (viewError && viewError !== this.toldViewError) notify({ level: 'error', message: viewError })
|
|
588
|
-
this.toldViewError = viewError
|
|
589
|
-
}
|
|
590
|
-
/*
|
|
591
|
-
An edit that would stop clips is refused by figure-model with the clips named (CLIPS_DROPPED). The person is told
|
|
592
|
-
which, and why, before anything is written; agreed, the same edit is made again letting exactly those go, as one
|
|
593
|
-
step undo brings back (designer 2026-09-27).
|
|
594
|
-
*/
|
|
595
|
-
private askClipLoss(error: unknown, again: () => void): boolean {
|
|
596
|
-
if ((error as { code?: string })?.code !== 'CLIPS_DROPPED') return false
|
|
597
|
-
const clips = (error as { clips: FigureClipLoss[] }).clips
|
|
598
|
-
/* The consent is for this edit, on this figure as it is now: held with it while the person reads. */
|
|
599
|
-
const session = this.session!
|
|
600
|
-
const { context, revision } = session.read()
|
|
601
|
-
void this.confirmClipLoss(clips, again, { session, context: context!, revision: revision! })
|
|
602
|
-
return true
|
|
603
|
-
}
|
|
604
|
-
private async confirmClipLoss(clips: FigureClipLoss[], again: () => void, asked: { session: FigureProductSession; context: string; revision: number }) {
|
|
605
|
-
const agreed = await OxPrompt.open({
|
|
606
|
-
type: 'warning',
|
|
607
|
-
text: i18next.t('figure.text.clips-would-go', { clips: clips.map(c => i18next.t(`figure.text.clip-${c.why}`, { clip: c.clip })).join(', ') }),
|
|
608
|
-
confirmButton: { text: i18next.t('figure.button.let-clips-go') },
|
|
609
|
-
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
610
|
-
})
|
|
611
|
-
if (!agreed) return
|
|
612
|
-
/* Changed while the person read -- another figure, or an edit in between: the consent was not for this; nothing is applied. */
|
|
613
|
-
const now = this.session === asked.session ? asked.session.read() : undefined
|
|
614
|
-
if (!now || now.context !== asked.context || now.revision !== asked.revision) {
|
|
615
|
-
this.error = i18next.t('figure.text.clips-consent-stale')
|
|
616
|
-
return
|
|
617
|
-
}
|
|
618
|
-
asked.session.withConsent({ dropClips: clips.map(c => c.id), context: asked.context, revision: asked.revision }, again)
|
|
619
|
-
}
|
|
620
|
-
private async ask(message: string, retry: { label: string; action?: FigureAuthoringAction; run?: () => void }) {
|
|
621
|
-
const agreed = await OxPrompt.open({
|
|
622
|
-
type: 'question',
|
|
623
|
-
text: message,
|
|
624
|
-
confirmButton: { text: retry.label },
|
|
625
|
-
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
626
|
-
})
|
|
627
|
-
if (!agreed) return
|
|
628
|
-
if (retry.run) retry.run()
|
|
629
|
-
else this.linkCommand(retry.action!)
|
|
630
|
-
}
|
|
631
|
-
/*
|
|
632
|
-
Per model and per part, how far its properties panel was scrolled and which of its folds were open, so going back
|
|
633
|
-
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
|
|
634
|
-
hide which part the panel speaks of. Another model starts clean.
|
|
635
|
-
*/
|
|
636
|
-
private panelMemory = new Map<string, { top: number; open: boolean[] }>()
|
|
637
|
-
private restorePanelFor?: string
|
|
638
|
-
private propertiesPanel() {
|
|
639
|
-
return this.renderRoot.querySelector<HTMLElement>('div[panel]')
|
|
640
|
-
}
|
|
641
|
-
private foldsOf(panel: HTMLElement): HTMLDetailsElement[] {
|
|
642
|
-
const folds = [...panel.querySelectorAll('details')]
|
|
643
|
-
for (const inner of panel.querySelectorAll('figure-links')) folds.push(...(inner.shadowRoot?.querySelectorAll('details') ?? []))
|
|
644
|
-
return folds
|
|
645
|
-
}
|
|
215
|
+
private paneFold = new PaneFold(this, () => this.panes)
|
|
216
|
+
private refusals = new RefusalTeller({
|
|
217
|
+
session: () => this.session,
|
|
218
|
+
error: () => this.error,
|
|
219
|
+
notice: () => this.notice,
|
|
220
|
+
retry: () => this.retry,
|
|
221
|
+
refuse: message => (this.error = message),
|
|
222
|
+
command: action => this.linkCommand(action)
|
|
223
|
+
})
|
|
224
|
+
private panelMemory = new PanelMemory(() => this.renderRoot)
|
|
646
225
|
/** The preview is made when first asked for, and kept (hidden) after that; it is taken down with the editor. */
|
|
647
226
|
private previewMounted = false
|
|
648
227
|
protected willUpdate(_changed: PropertyValues) {
|
|
@@ -655,10 +234,7 @@ export class FigureEditor extends LitElement {
|
|
|
655
234
|
this.hiddenParts = new Set()
|
|
656
235
|
this.panelMemory.clear()
|
|
657
236
|
} else if (_changed.has('selected')) {
|
|
658
|
-
|
|
659
|
-
const was = (_changed.get('selected') as string | undefined) ?? ''
|
|
660
|
-
if (panel) this.panelMemory.set(was || '@model', { top: panel.scrollTop, open: this.foldsOf(panel).map(d => d.open) })
|
|
661
|
-
this.restorePanelFor = this.selected || '@model'
|
|
237
|
+
this.panelMemory.leave((_changed.get('selected') as string | undefined) ?? '', this.selected)
|
|
662
238
|
}
|
|
663
239
|
if (view && !view.geometry.some(g => ownerOf(g.id) === this.selected)) this.selected = ''
|
|
664
240
|
if (_changed.has('selected')) this.gizmoMode = undefined
|
|
@@ -714,7 +290,7 @@ export class FigureEditor extends LitElement {
|
|
|
714
290
|
this.notice = ''
|
|
715
291
|
this.changed()
|
|
716
292
|
} catch (error) {
|
|
717
|
-
if (this.askClipLoss(error, () => this.run(action))) return
|
|
293
|
+
if (this.refusals.askClipLoss(error, () => this.run(action))) return
|
|
718
294
|
this.error = this.said(error)
|
|
719
295
|
} finally {
|
|
720
296
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
@@ -864,7 +440,7 @@ export class FigureEditor extends LitElement {
|
|
|
864
440
|
if (this.selected === id) this.selected = ''
|
|
865
441
|
this.changed()
|
|
866
442
|
} catch (error) {
|
|
867
|
-
if (this.askClipLoss(error, () => this.removePart(id, refitRoom))) return
|
|
443
|
+
if (this.refusals.askClipLoss(error, () => this.removePart(id, refitRoom))) return
|
|
868
444
|
/*
|
|
869
445
|
A part in the declared space goes, and the space is fitted to the parts left, in the same click: every space
|
|
870
446
|
this product declares is fitted to the parts (「부품에 맞추기」, the AI's fit-room -- no padding), so fitting it
|
|
@@ -1136,19 +712,6 @@ export class FigureEditor extends LitElement {
|
|
|
1136
712
|
}
|
|
1137
713
|
}
|
|
1138
714
|
/** Size response, fastening and motion for the selected part, as authoring commands. */
|
|
1139
|
-
/*
|
|
1140
|
-
What a panel is handed is the same object while it says the same thing: a new array each render reads as a change to
|
|
1141
|
-
the panel, which draws itself again for nothing (sixteen times for one edit).
|
|
1142
|
-
*/
|
|
1143
|
-
private previewWordsSaid?: { preview: unknown; part: string; language: string; words: string[] }
|
|
1144
|
-
private previewWordsNow(): string[] {
|
|
1145
|
-
const preview = this.currentPreview()
|
|
1146
|
-
const language = i18next.language
|
|
1147
|
-
if (this.previewWordsSaid && this.previewWordsSaid.preview === preview && this.previewWordsSaid.part === this.selected && this.previewWordsSaid.language === language) return this.previewWordsSaid.words
|
|
1148
|
-
const words = previewWordsOf(preview, this.selected, (key, params) => i18next.t(key, params))
|
|
1149
|
-
this.previewWordsSaid = { preview, part: this.selected, language, words }
|
|
1150
|
-
return words
|
|
1151
|
-
}
|
|
1152
715
|
private renderLinks(unavailable: boolean, section: 'shape' | 'places' | 'motion') {
|
|
1153
716
|
const shown = this.shown()
|
|
1154
717
|
if (!shown) return null
|
|
@@ -1158,13 +721,13 @@ export class FigureEditor extends LitElement {
|
|
|
1158
721
|
.partId=${this.selected}
|
|
1159
722
|
?locked=${unavailable}
|
|
1160
723
|
@command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
1161
|
-
.previewWords=${this.
|
|
1162
|
-
.detachWords=${this.detachWords(shown)}
|
|
724
|
+
.previewWords=${this.commandPreview.words()}
|
|
725
|
+
.detachWords=${this.commandPreview.detachWords(shown)}
|
|
1163
726
|
.overhangWords=${section === 'shape' ? this.overhangOf(shown).words : NO_WORDS}
|
|
1164
727
|
.overhangNote=${section === 'shape' ? this.overhangOf(shown).note : NO_WORDS}
|
|
1165
728
|
.motionEnds=${section === 'motion' ? this.motionEnds() : null}
|
|
1166
729
|
@go-fasten=${() => this.goFasten()}
|
|
1167
|
-
@preview-command=${(e: CustomEvent) => this.
|
|
730
|
+
@preview-command=${(e: CustomEvent) => this.commandPreview.ask(e.detail)}
|
|
1168
731
|
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
1169
732
|
></figure-links>`
|
|
1170
733
|
}
|
|
@@ -1239,7 +802,7 @@ export class FigureEditor extends LitElement {
|
|
|
1239
802
|
this.changed()
|
|
1240
803
|
return true
|
|
1241
804
|
} catch (error) {
|
|
1242
|
-
if (this.askClipLoss(error, () => this.linkCommand(action, from))) return false
|
|
805
|
+
if (this.refusals.askClipLoss(error, () => this.linkCommand(action, from))) return false
|
|
1243
806
|
const code = (error as { code?: string })?.code
|
|
1244
807
|
/* Something stands on a face this takes away: ask where it goes, there, rather than only saying no. */
|
|
1245
808
|
const need = (error as { details?: FigureFaceMappingNeed }).details
|
|
@@ -1637,117 +1200,6 @@ export class FigureEditor extends LitElement {
|
|
|
1637
1200
|
await this.updateComplete
|
|
1638
1201
|
await this.querySelector<HTMLElement & { showRelease?: () => Promise<void> }>('figure-history-panel')?.showRelease?.()
|
|
1639
1202
|
}
|
|
1640
|
-
/** The fastening form's command, seen before it is done; null clears what was shown. */
|
|
1641
|
-
/**
|
|
1642
|
-
* What a command would do is worked out on a copy of the figure -- several compiles of it -- so it waits for the field to
|
|
1643
|
-
* rest: a number typed digit by digit asks once, for the number it ends on. Clearing is at once, and the same question on
|
|
1644
|
-
* the same version is not asked again.
|
|
1645
|
-
*/
|
|
1646
|
-
private previewTimer?: ReturnType<typeof setTimeout>
|
|
1647
|
-
private previewCommand(candidate: { action: FigureAuthoringAction; face?: { part: string; face: string } } | null) {
|
|
1648
|
-
clearTimeout(this.previewTimer)
|
|
1649
|
-
if (!candidate || !this.session || !this.selected) {
|
|
1650
|
-
this.preview = undefined
|
|
1651
|
-
return
|
|
1652
|
-
}
|
|
1653
|
-
const view = this.session.read()
|
|
1654
|
-
const key = JSON.stringify(candidate)
|
|
1655
|
-
const same = this.preview && this.preview.key === key && this.preview.context === view.context && this.preview.part === this.selected && 'revision' in this.preview && this.preview.revision === view.revision
|
|
1656
|
-
if (same) return
|
|
1657
|
-
/*
|
|
1658
|
-
What another command would do is not said of this one while this one is worked out: a row's pitch refused stayed
|
|
1659
|
-
on screen for the count chosen after it (:3300, 2026-10-11).
|
|
1660
|
-
*/
|
|
1661
|
-
if (this.preview && this.preview.key !== key) this.preview = undefined
|
|
1662
|
-
this.previewTimer = setTimeout(() => this.computePreview(candidate), PREVIEW_SETTLE_MS)
|
|
1663
|
-
}
|
|
1664
|
-
private computePreview(candidate: { action: FigureAuthoringAction; face?: { part: string; face: string } }) {
|
|
1665
|
-
if (!this.session || !this.selected) return
|
|
1666
|
-
const view = this.session.read()
|
|
1667
|
-
const held = { context: view.context!, revision: view.revision!, key: JSON.stringify(candidate), part: this.selected }
|
|
1668
|
-
try {
|
|
1669
|
-
const seen = this.session.previewCommand(candidate.action)
|
|
1670
|
-
let face: FigureFaceArea | undefined
|
|
1671
|
-
if (candidate.face) {
|
|
1672
|
-
try {
|
|
1673
|
-
const shown = this.shown()!
|
|
1674
|
-
face = figurePartFaceOf(shown.asset, candidate.face.part, candidate.face.face, shown.inputs)
|
|
1675
|
-
} catch {
|
|
1676
|
-
face = undefined
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
/* A row is read on the figure the command would make, by figure-model, as drawn. */
|
|
1680
|
-
const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.shown()!.inputs) : undefined
|
|
1681
|
-
this.preview = {
|
|
1682
|
-
...held,
|
|
1683
|
-
context: seen.context,
|
|
1684
|
-
revision: seen.revision,
|
|
1685
|
-
geometry: seen.geometry,
|
|
1686
|
-
moves: seen.moves,
|
|
1687
|
-
added: seen.added,
|
|
1688
|
-
...(row ? { row } : {}),
|
|
1689
|
-
clipsWouldGo: seen.clipsWouldGo,
|
|
1690
|
-
face,
|
|
1691
|
-
faceUnknown: !!candidate.face && !face
|
|
1692
|
-
}
|
|
1693
|
-
} catch (error) {
|
|
1694
|
-
/* Refused: nothing of an earlier result stays on screen, and the reason is said. */
|
|
1695
|
-
this.preview = { ...held, refused: this.said(error) }
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
/** The preview, while it still speaks of what is on screen: this figure, this version, this part. */
|
|
1699
|
-
private currentPreview(): (HeldPreview & { part: string }) | undefined {
|
|
1700
|
-
const p = this.preview
|
|
1701
|
-
const view = this.session?.read()
|
|
1702
|
-
/* Only where its form is: the properties panel stays in the page on another tab, and did not clear it (2026-09-27, :3391). */
|
|
1703
|
-
if (!p || !view || p.context !== view.context || p.revision !== view.revision || p.part !== this.selected || this.middle !== 'edit' || this.tab !== 'properties') return undefined
|
|
1704
|
-
return p
|
|
1705
|
-
}
|
|
1706
|
-
/*
|
|
1707
|
-
What taking the selected part off would do, worked out on a copy before the button is pressed (ADR-0097 §5.5): whether
|
|
1708
|
-
it stays where it stands now, what stays fastened to it, which clips would go. "It stays where it is" is said only
|
|
1709
|
-
when the copy shows it does.
|
|
1710
|
-
*/
|
|
1711
|
-
private detachTimer?: ReturnType<typeof setTimeout>
|
|
1712
|
-
private detachAsked?: { shown: FigureRevision; part: string }
|
|
1713
|
-
/**
|
|
1714
|
-
* Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
|
|
1715
|
-
* edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
|
|
1716
|
-
*/
|
|
1717
|
-
private detachWords(shown: FigureRevision): string[] {
|
|
1718
|
-
const part = this.selected
|
|
1719
|
-
if (!this.session || !shown.asset.joints?.some(j => j.body1.part === part)) return NO_WORDS
|
|
1720
|
-
const key = `detach|${part}`
|
|
1721
|
-
const said = derivedHeld<string[]>(shown, key)
|
|
1722
|
-
if (said) return said
|
|
1723
|
-
if (this.detachAsked?.shown !== shown || this.detachAsked.part !== part) {
|
|
1724
|
-
this.detachAsked = { shown, part }
|
|
1725
|
-
clearTimeout(this.detachTimer)
|
|
1726
|
-
this.detachTimer = setTimeout(() => {
|
|
1727
|
-
if (this.shown() !== shown || this.selected !== part) return
|
|
1728
|
-
derivedOf(shown, key, () => this.computeDetachWords(shown, part))
|
|
1729
|
-
this.requestUpdate()
|
|
1730
|
-
}, PREVIEW_SETTLE_MS)
|
|
1731
|
-
}
|
|
1732
|
-
return NO_WORDS
|
|
1733
|
-
}
|
|
1734
|
-
private computeDetachWords(shown: FigureRevision, part: string): string[] {
|
|
1735
|
-
const say = (k: string, params?: Record<string, string>) => i18next.t(k, params)
|
|
1736
|
-
let words: string[]
|
|
1737
|
-
try {
|
|
1738
|
-
const seen = this.session!.previewCommand({ kind: 'detach', part })
|
|
1739
|
-
const own = seen.moves.some(m => m.body === part)
|
|
1740
|
-
words = own
|
|
1741
|
-
? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key: `detach|${part}`, moves: seen.moves }, part, say)]
|
|
1742
|
-
: /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ []
|
|
1743
|
-
const children = (shown.asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => partLabelOf(j.body1.part))
|
|
1744
|
-
if (children.length) words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }))
|
|
1745
|
-
if (seen.clipsWouldGo.length) words.push(say('figure.text.detach-clips', { clips: seen.clipsWouldGo.map(c => c.clip).join(', ') }))
|
|
1746
|
-
} catch (error) {
|
|
1747
|
-
words = [say('figure.text.detach-refused', { reason: this.said(error) })]
|
|
1748
|
-
}
|
|
1749
|
-
return words
|
|
1750
|
-
}
|
|
1751
1203
|
/*
|
|
1752
1204
|
Past the edge of the face a part is fastened to (ADR-0097 §5.5, Q3): the selected part's own, and the parts fastened
|
|
1753
1205
|
to it -- said apart from the room, nothing moved. Read once per version, part and language. The note says it is wider
|
|
@@ -1803,12 +1255,10 @@ export class FigureEditor extends LitElement {
|
|
|
1803
1255
|
}
|
|
1804
1256
|
/** What the canvas draws of it, made once per preview: a new list every render would stand the board up each time. */
|
|
1805
1257
|
private previewShown(): { ghost: FigureRenderPart[]; highlight: FigureRenderPart | null } {
|
|
1806
|
-
const p = this.
|
|
1258
|
+
const p = this.commandPreview.current()
|
|
1807
1259
|
/* On the motion tab, with nothing being tried: where the selected part's motion takes it, at both ends. */
|
|
1808
1260
|
if (!p && this.tab === 'motion' && this.middle === 'edit') return { ghost: this.motionEnds()?.ghost ?? [], highlight: null }
|
|
1809
|
-
|
|
1810
|
-
if (this.previewDrawn?.of !== p) this.previewDrawn = { of: p, ghost: ghostPartsOf(p), highlight: faceHighlightOf(p.face) }
|
|
1811
|
-
return this.previewDrawn
|
|
1261
|
+
return this.commandPreview.drawnOf(p)
|
|
1812
1262
|
}
|
|
1813
1263
|
/**
|
|
1814
1264
|
* What the author has in view, for the AI dock to hear with a request (ADR-0097 §5.6 item 5): the part selected and
|
|
@@ -1851,23 +1301,12 @@ export class FigureEditor extends LitElement {
|
|
|
1851
1301
|
}
|
|
1852
1302
|
updated(changed: PropertyValues<this>) {
|
|
1853
1303
|
super.updated?.(changed)
|
|
1854
|
-
this.tell(changed)
|
|
1855
|
-
|
|
1856
|
-
const key = this.restorePanelFor
|
|
1857
|
-
this.restorePanelFor = undefined
|
|
1858
|
-
requestAnimationFrame(async () => {
|
|
1859
|
-
const panel = this.propertiesPanel()
|
|
1860
|
-
if (!panel || (this.selected || '@model') !== key) return
|
|
1861
|
-
await Promise.all([...panel.querySelectorAll<HTMLElement & { updateComplete?: Promise<unknown> }>('figure-links')].map(el => el.updateComplete))
|
|
1862
|
-
const kept = this.panelMemory.get(key)
|
|
1863
|
-
if (kept) this.foldsOf(panel).forEach((fold, i) => i < kept.open.length && (fold.open = kept.open[i]!))
|
|
1864
|
-
panel.scrollTop = kept?.top ?? 0
|
|
1865
|
-
})
|
|
1866
|
-
}
|
|
1304
|
+
this.refusals.tell(changed)
|
|
1305
|
+
this.panelMemory.restore(() => this.selected)
|
|
1867
1306
|
const panes = this.renderRoot.querySelector<OxThreePane>('ox-three-pane') ?? undefined
|
|
1868
1307
|
if (panes !== this.panes) {
|
|
1869
1308
|
this.panes = panes
|
|
1870
|
-
this.
|
|
1309
|
+
this.paneFold.fitTo(this.getBoundingClientRect().width)
|
|
1871
1310
|
}
|
|
1872
1311
|
}
|
|
1873
1312
|
/**
|