@things-factory/figure-ui 10.1.204 → 10.1.206
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/canvas.ts +91 -9
- package/client/modeller/editor-styles.ts +23 -22
- package/client/modeller/editor.ts +217 -184
- package/client/modeller/fasten-form.ts +130 -12
- package/client/modeller/figure-fasten-dialog.ts +45 -11
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-links.ts +2 -2
- package/client/modeller/figure-preview.ts +6 -6
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +22 -1
- package/client/modeller/keys.ts +10 -1
- package/client/modeller/links.ts +79 -46
- package/client/modeller/palette.ts +1 -1
- package/client/modeller/product-session.ts +16 -4
- package/client/modeller/refusal-teller.ts +3 -25
- package/client/modeller/refusals.ts +19 -26
- package/client/modeller/section-drawing.ts +7 -7
- package/client/modeller/size-drag.ts +112 -0
- package/client/modeller/slot-layout-host.ts +3 -3
- package/client/modeller/stage-model.ts +6 -9
- package/client/pages/figure-modeller-page.ts +1 -4
- package/dist-client/modeller/canvas.d.ts +24 -3
- package/dist-client/modeller/canvas.js +96 -11
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +23 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +35 -35
- package/dist-client/modeller/editor.js +221 -193
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +46 -4
- package/dist-client/modeller/fasten-form.js +120 -11
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +18 -4
- package/dist-client/modeller/figure-fasten-dialog.js +49 -12
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +33 -1
- package/dist-client/modeller/figure-fasten-drawing.js +235 -11
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +2 -2
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +4 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +3 -3
- package/dist-client/modeller/figure-section-editor.js +4 -4
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +18 -1
- package/dist-client/modeller/gizmo.js +19 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/keys.d.ts +1 -1
- package/dist-client/modeller/keys.js +6 -0
- package/dist-client/modeller/keys.js.map +1 -1
- package/dist-client/modeller/links.d.ts +30 -24
- package/dist-client/modeller/links.js +67 -42
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/palette.js +1 -1
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +15 -13
- package/dist-client/modeller/product-session.js +15 -5
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +1 -12
- package/dist-client/modeller/refusal-teller.js +3 -22
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +6 -14
- package/dist-client/modeller/refusals.js +16 -27
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +3 -4
- package/dist-client/modeller/section-drawing.js +10 -9
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/size-drag.d.ts +38 -0
- package/dist-client/modeller/size-drag.js +85 -0
- package/dist-client/modeller/size-drag.js.map +1 -0
- package/dist-client/modeller/slot-layout-host.d.ts +1 -3
- package/dist-client/modeller/slot-layout-host.js +3 -3
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +5 -7
- package/dist-client/modeller/stage-model.js +6 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +0 -2
- package/dist-client/pages/figure-modeller-page.js +1 -6
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +7 -11
- package/test/browser/acceptance-page.js +11 -1
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +124 -69
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +77 -26
- package/test/browser/parts.check.mjs +49 -0
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +29 -2
- package/test/fasten-form.test.ts +102 -1
- package/test/gizmo.test.ts +25 -16
- package/test/keys.test.ts +11 -0
- package/test/links.test.ts +46 -36
- package/test/refusals.test.ts +17 -21
- package/test/resize-with-placement.test.ts +14 -13
- package/test/revision.test.ts +3 -2
- package/test/section-editor.test.ts +9 -10
- package/test/size-drag.test.ts +114 -0
- package/test/slot-layout-host.test.ts +13 -8
- package/translations/en.json +32 -34
- package/translations/es.json +32 -34
- package/translations/ja.json +32 -34
- package/translations/ko.json +32 -34
- package/translations/ms.json +32 -34
- package/translations/zh.json +32 -34
|
@@ -6,7 +6,7 @@ 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 { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor,
|
|
9
|
+
import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditOf, figureSizeEditorOf, 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'
|
|
@@ -30,12 +30,13 @@ import './figure-preview.js'
|
|
|
30
30
|
import './figure-settings.js'
|
|
31
31
|
import { DEFAULT_VIEW, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
32
32
|
import './figure-capability-panel.js'
|
|
33
|
-
import { gizmoChanges, gizmoReachOf, type GizmoReach, type GizmoResult } from './gizmo.js'
|
|
33
|
+
import { gizmoChanges, gizmoReachOf, sameMeasureAxesOf, sizeKeepOf, type GizmoReach, type GizmoResult } from './gizmo.js'
|
|
34
|
+
import { keptBySizeOf, partsAtFrame, SIZE_KEEPS_WORDS, startSizeDrag, throughPlaneOf, type SizeDrag, type SizeDragFrame } from './size-drag.js'
|
|
34
35
|
import type { KeyAction } from './keys.js'
|
|
35
36
|
import { ModellerKeys } from './modeller-keys.js'
|
|
36
37
|
import { PaneFold } from './pane-fold.js'
|
|
37
38
|
import { PanelMemory } from './panel-memory.js'
|
|
38
|
-
import { RefusalTeller
|
|
39
|
+
import { RefusalTeller } from './refusal-teller.js'
|
|
39
40
|
import { groupFields, ownerOf, partLabelOf, repeatOf, type FieldLike, type FieldGroup } from './fields.js'
|
|
40
41
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
41
42
|
import { EditorStyles } from './editor-styles.js'
|
|
@@ -52,17 +53,18 @@ const NO_MARKS: FigureRenderPart[] = []
|
|
|
52
53
|
import { apexOver, drawnOf } from './section-drawing.js'
|
|
53
54
|
import { END_FACE } from './section-controls.js'
|
|
54
55
|
import type { FigureCanvas } from './canvas.js'
|
|
55
|
-
import { nudgeOf, placementOf
|
|
56
|
+
import { nudgeOf, placementOf } from './stage-model.js'
|
|
56
57
|
|
|
57
58
|
/** The numbers a rule gives a part's position, turn and size, per axis, for the cells that have no field. */
|
|
58
59
|
type HeldValues = { position?: number[]; rotation?: number[]; size?: (number | undefined)[]; sizeRuled?: boolean }
|
|
59
60
|
import { occupancyFromParts, roomRefitRowsOf } from './occupancy.js'
|
|
60
61
|
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
61
62
|
import type { FigureGateFinding } from '../types.js'
|
|
62
|
-
import { editRefusalOf, inDeclaredRoom,
|
|
63
|
+
import { editRefusalOf, inDeclaredRoom, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js'
|
|
63
64
|
import type { FigureRenderPart } from './renderer.js'
|
|
64
65
|
import type { FigureBudgetLine } from './maturity.js'
|
|
65
|
-
import {
|
|
66
|
+
import { HelpToggles } from './help.js'
|
|
67
|
+
import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceSizeToDeclare, proportionToFollow, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
|
|
66
68
|
import { jointFanOf } from './joint-fan.js'
|
|
67
69
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
68
70
|
import { jointDragOf } from './joint-drag.js'
|
|
@@ -114,8 +116,8 @@ export class FigureEditor extends LitElement {
|
|
|
114
116
|
/** Why the last recipe was refused, in the kernel's words. */
|
|
115
117
|
@state() private recipeError = ''
|
|
116
118
|
@property({ type: Number }) revision = 0
|
|
117
|
-
/** The
|
|
118
|
-
|
|
119
|
+
/** The 「?」 beside a group's title and the sentence it opens. */
|
|
120
|
+
private readonly help = new HelpToggles(this)
|
|
119
121
|
/** The cost line the parts list shows, from the maturity report. */
|
|
120
122
|
@property({ attribute: false }) budget?: FigureBudgetLine
|
|
121
123
|
/**
|
|
@@ -133,18 +135,25 @@ export class FigureEditor extends LitElement {
|
|
|
133
135
|
* edit was made (`from`).
|
|
134
136
|
*/
|
|
135
137
|
@state() private faceMapping?: { action: FigureAuthoringAction; need: FigureFaceMappingNeed; from: 'section-editor' | 'panel' }
|
|
136
|
-
/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
|
|
137
|
-
@state() private retry?: Retry
|
|
138
138
|
@state() private tab: SideTab = 'properties'
|
|
139
139
|
/** The view settings or the release history, open over the work from the bar. */
|
|
140
140
|
@state() private overlay?: BarOverlay
|
|
141
141
|
/**
|
|
142
|
-
* The fastening window open for the selected part (user 2026-10-17)
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
142
|
+
* The fastening window open for the selected part (user 2026-10-17), in the standard popup, moved by its head
|
|
143
|
+
* (2026-10-18; put back 2026-10-20 -- the side panel it was moved to could not be moved), with no backdrop, so the
|
|
144
|
+
* canvas can be pressed while it is open: chosen there, applied once. It closes with nothing written when the part,
|
|
145
|
+
* the tab or the middle changes under it.
|
|
146
146
|
*/
|
|
147
147
|
@state() private fastening = false
|
|
148
|
+
private fastenPopup?: { close: () => void }
|
|
149
|
+
private fastenWindow?: HTMLElement & {
|
|
150
|
+
revision?: FigureRevision
|
|
151
|
+
partId: string
|
|
152
|
+
locked: boolean
|
|
153
|
+
previewWords: string[]
|
|
154
|
+
error: string
|
|
155
|
+
aimAt?: (at: { id: string; point?: number[]; normal?: number[] }) => void
|
|
156
|
+
}
|
|
148
157
|
/** Why the window's 「적용」 was refused, said in it. */
|
|
149
158
|
@state() private fastenError = ''
|
|
150
159
|
/** The face of the part a row of the window's list is on, lit on the canvas while the pointer or the arrows are there. */
|
|
@@ -205,6 +214,8 @@ export class FigureEditor extends LitElement {
|
|
|
205
214
|
@state() private hiddenParts = new Set<string>()
|
|
206
215
|
/** A value in hand -- typed, held on a spinner, scrubbed, dragged -- drawn at once and recorded on release. */
|
|
207
216
|
@state() private live: LiveValues = NOTHING_LIVE
|
|
217
|
+
/** A size drag in hand, drawn from figure-model each frame (ADR-0140 4.4); gone with the release. */
|
|
218
|
+
@state() private sizing?: { part: string; drag: SizeDrag; frame?: SizeDragFrame; through?: number }
|
|
208
219
|
/** The part whose drawn section is open in the section editor (ADR-0100 §3.2), or null. */
|
|
209
220
|
@state() private sectionEditing: string | null = null
|
|
210
221
|
private lastContext: string | null = null
|
|
@@ -234,7 +245,7 @@ export class FigureEditor extends LitElement {
|
|
|
234
245
|
aria-pressed=${String(this.gizmoMode === tool.mode)}
|
|
235
246
|
?disabled=${!tool.can}
|
|
236
247
|
aria-label=${i18next.t('figure.button.gizmo-' + tool.mode)}
|
|
237
|
-
title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' +
|
|
248
|
+
title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + i18next.t('figure.text.gizmo-not-own')}`}
|
|
238
249
|
@click=${() => this.useGizmo(tool.mode)}
|
|
239
250
|
>
|
|
240
251
|
<md-icon>${tool.icon}</md-icon>
|
|
@@ -255,6 +266,14 @@ export class FigureEditor extends LitElement {
|
|
|
255
266
|
selected: () => this.selected,
|
|
256
267
|
act: action => this.keyAction(action)
|
|
257
268
|
})
|
|
269
|
+
/** The part Ctrl+C took: pasted as the kernel's duplicate. Not kept past the editor, nor written anywhere. */
|
|
270
|
+
private copied = ''
|
|
271
|
+
/** A copied part still in the figure is duplicated -- a part of its own, named by figure-model; one gone, nothing. */
|
|
272
|
+
private pasteCopied() {
|
|
273
|
+
const asset = this.session?.revisionNow()?.asset
|
|
274
|
+
if (!this.copied || !asset?.document.model.nodes.some(n => n.op === 'place@1' && n.id === this.copied)) return
|
|
275
|
+
this.duplicatePart(this.copied)
|
|
276
|
+
}
|
|
258
277
|
private keyAction(action: KeyAction) {
|
|
259
278
|
const view = this.session!.read()
|
|
260
279
|
const canvas = this.renderRoot.querySelector<FigureCanvas>('figure-canvas')
|
|
@@ -262,6 +281,9 @@ export class FigureEditor extends LitElement {
|
|
|
262
281
|
else if (action === 'undo') view.canUndo && this.run(() => this.session!.undo())
|
|
263
282
|
else if (action === 'redo') view.canRedo && this.run(() => this.session!.redo())
|
|
264
283
|
else if (action === 'duplicate') this.duplicatePart(this.selected)
|
|
284
|
+
else if (action === 'copy') this.copied = this.selected
|
|
285
|
+
else if (action === 'paste') this.pasteCopied()
|
|
286
|
+
else if (action === 'hold') return
|
|
265
287
|
else if (action === 'remove') this.removePart(this.selected)
|
|
266
288
|
else this.selected = ''
|
|
267
289
|
}
|
|
@@ -270,9 +292,7 @@ export class FigureEditor extends LitElement {
|
|
|
270
292
|
session: () => this.session,
|
|
271
293
|
error: () => this.error,
|
|
272
294
|
notice: () => this.notice,
|
|
273
|
-
|
|
274
|
-
refuse: message => (this.error = message),
|
|
275
|
-
command: action => this.linkCommand(action)
|
|
295
|
+
refuse: message => (this.error = message)
|
|
276
296
|
})
|
|
277
297
|
private panelMemory = new PanelMemory(() => this.renderRoot)
|
|
278
298
|
/** The preview is made when first asked for, and kept (hidden) after that; it is taken down with the editor. */
|
|
@@ -292,7 +312,7 @@ export class FigureEditor extends LitElement {
|
|
|
292
312
|
if (view && !view.geometry.some(g => ownerOf(g.id) === this.selected)) this.selected = ''
|
|
293
313
|
if (_changed.has('selected')) this.gizmoMode = undefined
|
|
294
314
|
/* The window is about the part chosen on the properties tab, being edited: it does not follow anywhere else. */
|
|
295
|
-
if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit'
|
|
315
|
+
if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit')) this.closeFastening()
|
|
296
316
|
/*
|
|
297
317
|
* The section editor is of one part: when the selection moves off it by any way -- another part picked, the scene's
|
|
298
318
|
* own Esc, the part removed, another figure opened -- it closes and lets go of the preview it held.
|
|
@@ -318,17 +338,22 @@ export class FigureEditor extends LitElement {
|
|
|
318
338
|
if (closed && this.selected === editing) this.selected = next
|
|
319
339
|
})
|
|
320
340
|
}
|
|
341
|
+
/** While the size tool is on a round part: which of its handles change the one diameter (ADR-0140 4.1). */
|
|
342
|
+
private renderSameMeasure(unavailable: boolean) {
|
|
343
|
+
if (unavailable || this.gizmoMode !== 'scale' || !this.selected) return nothing
|
|
344
|
+
const axes = sameMeasureAxesOf(this.reachOf(this.selected))
|
|
345
|
+
return axes
|
|
346
|
+
? html`<p same-measure role="status">${i18next.t('figure.text.gizmo-same-measure', { axes: axes.map(a => a.toUpperCase()).join(' · ') })}</p>`
|
|
347
|
+
: nothing
|
|
348
|
+
}
|
|
321
349
|
/** The scene toggles: the mode already shown takes the gizmo away. */
|
|
322
350
|
private useGizmo(mode: 'translate' | 'rotate' | 'scale') {
|
|
323
351
|
/*
|
|
324
352
|
R does what the bar's resize tool does: on a part whose size is not its own to drag the tool is off, so no gizmo is
|
|
325
|
-
put on it to drag in vain (:3300, 10.1.196) -- the tool's own words are said instead
|
|
326
|
-
its size fields.
|
|
353
|
+
put on it to drag in vain (:3300, 10.1.196) -- the tool's own words are said instead.
|
|
327
354
|
*/
|
|
328
355
|
if (mode === 'scale' && this.selected && this.gizmoMode !== 'scale' && !this.reachOf(this.selected).scale) {
|
|
329
|
-
|
|
330
|
-
if (asset && this.sizeDragTurned(asset, this.selected)) this.tellSizeByFields(this.selected)
|
|
331
|
-
else this.refusals.say(i18next.t('figure.text.gizmo-not-own'))
|
|
356
|
+
this.refusals.say(i18next.t('figure.text.gizmo-not-own'))
|
|
332
357
|
return
|
|
333
358
|
}
|
|
334
359
|
this.renderRoot.querySelector<FigureCanvas>('figure-canvas')?.gizmo(mode)
|
|
@@ -354,6 +379,7 @@ export class FigureEditor extends LitElement {
|
|
|
354
379
|
} finally {
|
|
355
380
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
356
381
|
this.live = NOTHING_LIVE
|
|
382
|
+
this.sizing = undefined
|
|
357
383
|
}
|
|
358
384
|
}
|
|
359
385
|
/** The bodies drawn with the value in hand, or null when nothing is in hand (or the figure cannot take it). */
|
|
@@ -363,7 +389,8 @@ export class FigureEditor extends LitElement {
|
|
|
363
389
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
364
390
|
private drawnParts(geometry: Parameters<typeof drawableFigureParts>[0] | undefined, live: FigureRenderPart[] | null) {
|
|
365
391
|
const parts = live ?? drawableFigureParts(geometry ?? [])
|
|
366
|
-
|
|
392
|
+
const frame = this.sizing?.frame
|
|
393
|
+
return (frame ? partsAtFrame(parts, frame.moved) : parts).filter(body => !this.hiddenParts.has(ownerOf((body as { id: string }).id)))
|
|
367
394
|
}
|
|
368
395
|
/**
|
|
369
396
|
* Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
|
|
@@ -400,6 +427,9 @@ export class FigureEditor extends LitElement {
|
|
|
400
427
|
}
|
|
401
428
|
|
|
402
429
|
private changed() {
|
|
430
|
+
/* What the step did to the size ends the author typed, said with whatever else it said. */
|
|
431
|
+
const fits = this.session?.takeSizeRangeFits() ?? []
|
|
432
|
+
if (fits.length) this.notice = [this.notice, sizeRangeFitsOf(fits, (key, params) => i18next.t(key, params))].filter(Boolean).join(' ')
|
|
403
433
|
this.requestUpdate()
|
|
404
434
|
this.dispatchEvent(new CustomEvent('changed', { bubbles: true, composed: true }))
|
|
405
435
|
}
|
|
@@ -495,7 +525,6 @@ export class FigureEditor extends LitElement {
|
|
|
495
525
|
this.run(() => this.session!.edit({ kind: 'move-part', target: id, to }))
|
|
496
526
|
}
|
|
497
527
|
private removePart(id: string, refitRoom = false) {
|
|
498
|
-
this.retry = undefined
|
|
499
528
|
try {
|
|
500
529
|
const view = this.session!.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) }) as { removal?: Parameters<typeof removalNoticeOf>[0] }
|
|
501
530
|
this.error = ''
|
|
@@ -563,60 +592,42 @@ export class FigureEditor extends LitElement {
|
|
|
563
592
|
if (!id || !this.session || !this.session.read().geometry.some(g => g.id === id)) return none
|
|
564
593
|
try {
|
|
565
594
|
const asset = this.shown()!.asset
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
A size drag shows the face it keeps and the commit keeps the same (figureScaleAnchorOf). Where figure-model cannot
|
|
569
|
-
say which face that is -- a part fastened turned or twisted -- the drag would show one result and write another,
|
|
570
|
-
so sizing by the gizmo is not offered there; the fields still size it (designer 2026-09-27).
|
|
571
|
-
*/
|
|
572
|
-
if (reach.scale && this.sizeDragUnsure(asset, id)) return { ...reach, scale: false }
|
|
573
|
-
return reach
|
|
595
|
+
/* Turned any way, fastened or not: a size drag is drawn from figure-model's one size function, frame by frame (ADR-0140 4.4). */
|
|
596
|
+
return gizmoReachOf(asset, id)
|
|
574
597
|
} catch {
|
|
575
598
|
return none
|
|
576
599
|
}
|
|
577
600
|
}
|
|
578
601
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
* 2026-10-19, the fan's blade-2). Its tool says it is placed by something else, as before.
|
|
602
|
+
* A size drag in hand, a frame of it: the sizes the handle comes to, through figure-model's size function and the
|
|
603
|
+
* evaluator stepped to them, drawn and written nowhere (ADR-0140 4.4). Begun at the first frame, from the figure as it
|
|
604
|
+
* is drawn and the handle the scene says was held; the release writes the last frame's changes.
|
|
583
605
|
*/
|
|
584
|
-
private
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return false
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
/** Whether a size drag on this part could not show what is written: figure-model gives no face it keeps. */
|
|
592
|
-
private sizeDragUnsure(asset: FigureAsset, id: string): boolean {
|
|
606
|
+
private resizing(id: string, result: GizmoResult) {
|
|
607
|
+
const part = ownerOf(id)
|
|
608
|
+
const shown = this.shown()
|
|
609
|
+
if (!shown || !result.scale) return
|
|
593
610
|
try {
|
|
594
|
-
|
|
611
|
+
const drawn = drawableFigureParts(this.session!.read().geometry ?? [])
|
|
612
|
+
if (this.sizing?.part !== part)
|
|
613
|
+
this.sizing = { part, drag: startSizeDrag(shown.asset, part, shown.inputs, sizeKeepOf(result.grab)), through: throughPlaneOf(shown.asset, drawn, part) }
|
|
614
|
+
const sizes = gizmoChanges(id, this.reachOf(id), { scale: result.scale }, { ...shown.inputs })
|
|
615
|
+
this.sizing = { ...this.sizing, frame: this.sizing.drag.frame(sizes) }
|
|
595
616
|
} catch {
|
|
596
|
-
|
|
617
|
+
/* A size the figure cannot take on the way: the figure as it stands is drawn, and the release says why. */
|
|
618
|
+
this.sizing = undefined
|
|
597
619
|
}
|
|
598
620
|
}
|
|
599
621
|
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
622
|
+
* A free part a size change takes past the mounting plane: said, with 「…에 붙이기」 to fasten it there (ADR-0140 5). The
|
|
623
|
+
* size rule is not changed for it: what the author picks is the fastening, one step of the history.
|
|
602
624
|
*/
|
|
603
|
-
private
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
this.refusals.point(this.sizeDragRefusalOf(id), { label: i18next.t('figure.button.to-size-fields'), run: () => this.toSizeFields(id) })
|
|
610
|
-
}
|
|
611
|
-
/** To the part's first size field it takes typed, on the properties tab, ready to type over. */
|
|
612
|
-
private async toSizeFields(id: string) {
|
|
613
|
-
this.middle = 'edit'
|
|
614
|
-
this.tab = 'properties'
|
|
615
|
-
this.selected = id
|
|
616
|
-
await this.updateComplete
|
|
617
|
-
const field = this.renderRoot.querySelector<HTMLInputElement>('[slot="size"] input:not([disabled]):not([readonly])')
|
|
618
|
-
field?.focus()
|
|
619
|
-
field?.select()
|
|
625
|
+
private tellThroughPlane(part: string, mm: number) {
|
|
626
|
+
const plane = i18next.t('figure.label.resize-place-' + (this.asset()?.placement ?? 'floor'))
|
|
627
|
+
this.refusals.point(i18next.t('figure.text.size-through-plane', { part: partLabelOf(part), plane, mm: String(mm) }), {
|
|
628
|
+
label: i18next.t('figure.button.fasten-to-plane', { plane }),
|
|
629
|
+
run: () => this.run(() => this.session!.command({ kind: 'attach', part, to: { plane: 'mounting-plane' } }))
|
|
630
|
+
})
|
|
620
631
|
}
|
|
621
632
|
/** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
|
|
622
633
|
private transformed(id: string, result: GizmoResult) {
|
|
@@ -642,21 +653,21 @@ export class FigureEditor extends LitElement {
|
|
|
642
653
|
the shift had been taken for a move of the joint, and the size was dropped).
|
|
643
654
|
*/
|
|
644
655
|
const jointed = !!asset.joints?.some(j => j.body1.part === ownerOf(id))
|
|
645
|
-
/* A size drag this part cannot show truly is not written: it springs back, and the reason is said. */
|
|
646
|
-
if (result.scale && result.scale.some(k => Math.abs(k - 1) > 1e-4) && this.sizeDragTurned(asset, id)) {
|
|
647
|
-
this.tellSizeByFields(id)
|
|
648
|
-
return this.requestUpdate()
|
|
649
|
-
}
|
|
650
656
|
/*
|
|
651
|
-
A size dragged
|
|
652
|
-
|
|
657
|
+
A size dragged is written as its last frame was drawn: figure-model's one size function, keeping the side opposite
|
|
658
|
+
the handle held -- or, fastened, its fastening (ADR-0140 4.1 · 4.4). What the scene put the part at is not read.
|
|
653
659
|
*/
|
|
654
660
|
const scaled = !!result.scale && !result.rotation && result.scale.some(k => Math.abs(k - 1) > 1e-4)
|
|
655
661
|
const taken = jointed || scaled ? { scale: result.scale } : result
|
|
656
662
|
const written = gizmoChanges(id, this.reachOf(id), taken, sizes)
|
|
657
|
-
const
|
|
663
|
+
const drawnLast = this.sizing?.part === ownerOf(id) ? this.sizing.frame : undefined
|
|
664
|
+
const changes = !scaled ? written : drawnLast ? drawnLast.changes : figureSizeEditOf(asset, ownerOf(id), written, sizeKeepOf(result.grab)).changes
|
|
658
665
|
if (changes.length) {
|
|
666
|
+
const through = scaled ? (this.sizing?.part === ownerOf(id) ? this.sizing.through : undefined) ?? throughPlaneOf(asset, drawableFigureParts(view.geometry), ownerOf(id)) : 0
|
|
659
667
|
this.run(() => this.session!.apply(changes))
|
|
668
|
+
/* Taken past the mounting plane by the size it was given: said with how far, once it is written, and the way to fasten it (ADR-0140 5). */
|
|
669
|
+
const now = scaled ? throughPlaneOf(this.shown()!.asset, drawableFigureParts(this.session!.read().geometry ?? []), ownerOf(id)) : 0
|
|
670
|
+
if (now > through + 0.5) this.tellThroughPlane(ownerOf(id), now)
|
|
660
671
|
return
|
|
661
672
|
}
|
|
662
673
|
if (jointed && result.scale) {
|
|
@@ -850,6 +861,7 @@ export class FigureEditor extends LitElement {
|
|
|
850
861
|
${groups.singles.length
|
|
851
862
|
? html`<div form>${groups.singles.map(g => (g.kind === 'single' ? this.renderSingle(g.name, g.field, unavailable) : nothing))}</div>`
|
|
852
863
|
: nothing}
|
|
864
|
+
${this.renderSizeKeeps()}
|
|
853
865
|
</div>`
|
|
854
866
|
: nothing}
|
|
855
867
|
${place.length && !placeLocked
|
|
@@ -934,24 +946,6 @@ export class FigureEditor extends LitElement {
|
|
|
934
946
|
return html`${panel('view', 'figure.label.tab-environment', html`<figure-settings .view=${this.view} @update-view=${(e: CustomEvent) => this.setView(e.detail.view)}></figure-settings>`)}
|
|
935
947
|
${panel('history', 'figure.button.release', html`<slot name="history"></slot>`)}`
|
|
936
948
|
}
|
|
937
|
-
/**
|
|
938
|
-
* The face a size drag keeps for the selected part, figure-model's rule (`figureScaleAnchorOf`): the gizmo shows it
|
|
939
|
-
* while dragging and the commit keeps it, so nothing jumps on release. A part whose fastening is turned has none the
|
|
940
|
-
* rule can say, and the gizmo keeps its own (the face across from the handle).
|
|
941
|
-
*/
|
|
942
|
-
private anchorsOf(asset: FigureAsset | undefined): Record<string, ScaleAnchor> {
|
|
943
|
-
let next: Record<string, ScaleAnchor> = {}
|
|
944
|
-
try {
|
|
945
|
-
const anchor = asset && this.selected ? figureScaleAnchorOf(asset, this.selected) : undefined
|
|
946
|
-
if (anchor) next = { [this.selected]: anchor }
|
|
947
|
-
} catch {
|
|
948
|
-
next = {}
|
|
949
|
-
}
|
|
950
|
-
/* The same object while it says the same thing: the canvas stands its parts up again when this changes. */
|
|
951
|
-
if (JSON.stringify(next) !== JSON.stringify(this.lastAnchors)) this.lastAnchors = next
|
|
952
|
-
return this.lastAnchors
|
|
953
|
-
}
|
|
954
|
-
private lastAnchors: Record<string, ScaleAnchor> = {}
|
|
955
949
|
/** Data tried in the data tab, given to the preview's figures as their `data`; null when none is (ADR-0051 §8). */
|
|
956
950
|
@state() private dataSample: unknown = null
|
|
957
951
|
/** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
|
|
@@ -988,7 +982,6 @@ export class FigureEditor extends LitElement {
|
|
|
988
982
|
private linkCommand(action: FigureAuthoringAction, from: 'section-editor' | 'panel' = 'panel'): boolean {
|
|
989
983
|
/* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
|
|
990
984
|
this.holdAction(null)
|
|
991
|
-
this.retry = undefined
|
|
992
985
|
this.notice = ''
|
|
993
986
|
const hadSize = this.hasSize()
|
|
994
987
|
const hadSpace = !!this.shown()?.asset.occupancy
|
|
@@ -1019,12 +1012,6 @@ export class FigureEditor extends LitElement {
|
|
|
1019
1012
|
this.error = ''
|
|
1020
1013
|
return false
|
|
1021
1014
|
}
|
|
1022
|
-
const narrowing = narrowingRetryOf(action, error, (key, params) => i18next.t(key, params))
|
|
1023
|
-
if (narrowing) {
|
|
1024
|
-
this.error = narrowing.message
|
|
1025
|
-
this.retry = { label: narrowing.label, action: narrowing.retry as FigureAuthoringAction }
|
|
1026
|
-
return false
|
|
1027
|
-
}
|
|
1028
1015
|
this.error =
|
|
1029
1016
|
code === 'DIMENSION_CYCLE'
|
|
1030
1017
|
? i18next.t('figure.text.follow-would-loop')
|
|
@@ -1037,17 +1024,38 @@ export class FigureEditor extends LitElement {
|
|
|
1037
1024
|
private changeField(field: { id: string; unit: FigureInputChange['unit']; role?: string }, event: Event) {
|
|
1038
1025
|
const input = event.target as HTMLInputElement,
|
|
1039
1026
|
value = input.valueAsNumber
|
|
1040
|
-
/* A
|
|
1027
|
+
/* A number typed keeps the part's middle, or its fastening -- figure-model's one size function (ADR-0140 4.1). */
|
|
1041
1028
|
const part = this.selected
|
|
1042
1029
|
const change: FigureInputChange = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value }
|
|
1043
|
-
|
|
1030
|
+
const asset = this.shown()?.asset
|
|
1031
|
+
const before = asset && part ? throughPlaneOf(asset, drawableFigureParts(this.session!.read().geometry ?? []), part) : 0
|
|
1032
|
+
this.run(() => this.session!.apply(field.role !== 'state' && part ? figureSizeEditOf(this.shown()!.asset, part, [change], { keep: 'centre' }).changes : [change]))
|
|
1033
|
+
/* Typed past the mounting plane: said, with the way to fasten it there (ADR-0140 5). */
|
|
1034
|
+
const after = asset && part && field.role !== 'state' ? throughPlaneOf(this.shown()!.asset, drawableFigureParts(this.session!.read().geometry ?? []), part) : 0
|
|
1035
|
+
if (after > before + 0.5) this.tellThroughPlane(part, after)
|
|
1044
1036
|
// Rejected edits must also restore the displayed numeric value.
|
|
1045
1037
|
input.value = String(this.session!.read().fields.find(i => i.id === field.id)!.value)
|
|
1046
1038
|
}
|
|
1047
1039
|
/** Typing or a held spinner: each number on the way is drawn; the field's change records it. */
|
|
1048
|
-
private typing(field: { id: string }, event: Event) {
|
|
1040
|
+
private typing(field: { id: string; unit?: FigureInputChange['unit']; role?: string }, event: Event) {
|
|
1049
1041
|
const value = (event.target as HTMLInputElement).valueAsNumber
|
|
1050
|
-
if (Number.isFinite(value)) this.hold(
|
|
1042
|
+
if (Number.isFinite(value)) this.hold(this.heldOf(field, value))
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* The values in hand for a number typed or scrubbed: what its release writes -- a size with the part's middle kept,
|
|
1046
|
+
* or its fastening (figure-model's one size function, ADR-0140 4.1), so the figure does not jump on the release.
|
|
1047
|
+
*/
|
|
1048
|
+
private heldOf(field: { id: string; unit?: FigureInputChange['unit']; role?: string }, value: number, sized?: ReturnType<typeof figureSizeEditorOf>): Record<string, number> {
|
|
1049
|
+
const part = this.selected
|
|
1050
|
+
const asset = this.shown()?.asset
|
|
1051
|
+
if (field.role === 'state' || !part || !asset) return { [field.id]: value }
|
|
1052
|
+
const change: FigureInputChange = { kind: 'set-design', input: field.id, unit: field.unit ?? 'mm', value }
|
|
1053
|
+
try {
|
|
1054
|
+
const { changes } = sized ? sized([change], { keep: 'centre' }) : figureSizeEditOf(asset, part, [change], { keep: 'centre' })
|
|
1055
|
+
return Object.fromEntries(changes.map(c => [c.input, c.value as number]))
|
|
1056
|
+
} catch {
|
|
1057
|
+
return { [field.id]: value }
|
|
1058
|
+
}
|
|
1051
1059
|
}
|
|
1052
1060
|
/**
|
|
1053
1061
|
* Drag a field's label (or an axis letter) sideways to scrub its value, as vector tools do: the figure follows the
|
|
@@ -1062,10 +1070,13 @@ export class FigureEditor extends LitElement {
|
|
|
1062
1070
|
label.setPointerCapture(down.pointerId)
|
|
1063
1071
|
const start = field.value
|
|
1064
1072
|
let value = start
|
|
1073
|
+
/* Prepared once for the scrub: each move is a size through the same function its release writes with. */
|
|
1074
|
+
const asset = this.shown()?.asset
|
|
1075
|
+
const sized = asset && this.selected && field.role !== 'state' ? (() => { try { return figureSizeEditorOf(asset, this.selected) } catch { return undefined } })() : undefined
|
|
1065
1076
|
const move = (e: PointerEvent) => {
|
|
1066
1077
|
value = scrubbed(start, e.clientX - down.clientX, scrubStep(field.unit, start, e.shiftKey))
|
|
1067
1078
|
input.value = String(value)
|
|
1068
|
-
this.hold(
|
|
1079
|
+
this.hold(this.heldOf(field, value, sized))
|
|
1069
1080
|
}
|
|
1070
1081
|
const up = (e: PointerEvent) => {
|
|
1071
1082
|
label.removeEventListener('pointermove', move)
|
|
@@ -1085,75 +1096,82 @@ export class FigureEditor extends LitElement {
|
|
|
1085
1096
|
label.addEventListener('pointerup', up)
|
|
1086
1097
|
label.addEventListener('pointercancel', up)
|
|
1087
1098
|
}
|
|
1088
|
-
/** The sizes a board may give the figure, and the
|
|
1099
|
+
/** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
|
|
1089
1100
|
private renderInstanceRange(shown: FigureRevision | undefined, unavailable: boolean) {
|
|
1090
1101
|
if (!shown) return nothing
|
|
1091
1102
|
const asset = shown.asset
|
|
1092
|
-
/*
|
|
1103
|
+
/* Computed once per version: figure-model evaluates the figure at its edges. */
|
|
1093
1104
|
const measured = derivedOf(shown, 'instance-range', () => {
|
|
1094
1105
|
let of: ReturnType<typeof instanceRangeOf>
|
|
1095
|
-
let toDeclare: ReturnType<typeof
|
|
1106
|
+
let toDeclare: ReturnType<typeof instanceSizeToDeclare>
|
|
1096
1107
|
try {
|
|
1097
1108
|
of = instanceRangeOf(asset)
|
|
1098
1109
|
} catch {
|
|
1099
1110
|
of = undefined
|
|
1100
1111
|
}
|
|
1101
1112
|
try {
|
|
1102
|
-
toDeclare = of ? undefined :
|
|
1113
|
+
toDeclare = of ? undefined : instanceSizeToDeclare(asset)
|
|
1103
1114
|
} catch {
|
|
1104
1115
|
toDeclare = undefined
|
|
1105
1116
|
}
|
|
1106
1117
|
return { of, toDeclare }
|
|
1107
1118
|
})
|
|
1108
1119
|
const range = measured.of
|
|
1109
|
-
/*
|
|
1120
|
+
/*
|
|
1121
|
+
In words a person reads (user 2026-10-19/20): what the figure does on a drawing, not the inputs behind it. Fixed,
|
|
1122
|
+
it says the size it is always drawn at; resizable, the sizes it may take on each axis and the parts that change
|
|
1123
|
+
with it. The way to it stays one press -- a figure no part follows (a robot scaled whole) reaches the one-ratio
|
|
1124
|
+
rule only from here -- and the help mark says how parts are made to follow.
|
|
1125
|
+
*/
|
|
1126
|
+
const mm = (v: number) => Math.round(v).toLocaleString()
|
|
1110
1127
|
const toDeclare = measured.toDeclare
|
|
1128
|
+
const title = (explained: string) =>
|
|
1129
|
+
html`<h3>${i18next.t('figure.label.board-size')}${this.help.button(explained)}</h3>${this.help.hint(explained)}`
|
|
1111
1130
|
if (!range && toDeclare)
|
|
1112
|
-
return html`<section>
|
|
1113
|
-
|
|
1114
|
-
<p
|
|
1115
|
-
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size',
|
|
1116
|
-
<md-icon>
|
|
1131
|
+
return html`<section board-size>
|
|
1132
|
+
${title('figure.text.board-size-fixed-explained')}
|
|
1133
|
+
<p fixed-size>${i18next.t('figure.text.board-size-fixed', { x: mm(toDeclare.at.x), y: mm(toDeclare.at.y), z: mm(toDeclare.at.z) })}</p>
|
|
1134
|
+
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', at: toDeclare.at })}>
|
|
1135
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.declare-instance-size')}
|
|
1117
1136
|
</button>
|
|
1118
1137
|
</section>`
|
|
1119
1138
|
if (!range) return nothing
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1139
|
+
const followers = sizeFollowersOf(asset)
|
|
1140
|
+
/*
|
|
1141
|
+
An end the author typed shows its number; an open end is empty, with what the parts allow written faintly in it
|
|
1142
|
+
(ADR-0087 decision 6). Typing narrows that end, emptying the field opens it again.
|
|
1143
|
+
*/
|
|
1144
|
+
const shownEnd = (r: (typeof range)['x'], bound: 'min' | 'max') => (bound === 'max' && !Number.isFinite(r.max) ? '∞' : mm(r[bound]))
|
|
1145
|
+
return html`<section board-size>
|
|
1146
|
+
${title('figure.text.board-size-range-explained')}
|
|
1124
1147
|
${(['x', 'y', 'z'] as const).map(
|
|
1125
1148
|
axis => html`<div inline-field>
|
|
1126
|
-
<label>${
|
|
1127
|
-
<div pair>
|
|
1149
|
+
<label>${i18next.t('figure.label.resize-' + axis)} <span unit>mm</span></label>
|
|
1150
|
+
<div pair range>
|
|
1128
1151
|
${(['min', 'max'] as const).map(
|
|
1129
|
-
bound => html`<input
|
|
1152
|
+
(bound, i) => html`${i ? html`<span dash aria-hidden="true">–</span>` : nothing}<input
|
|
1130
1153
|
aria-label=${`size.${axis}.${bound}`}
|
|
1131
1154
|
type="number"
|
|
1132
1155
|
step="any"
|
|
1133
|
-
.value=${live(String(Math.round(range
|
|
1156
|
+
.value=${live(range[axis].narrowed[bound] ? String(Math.round(range[axis][bound])) : '')}
|
|
1157
|
+
placeholder=${shownEnd(range[axis], bound)}
|
|
1134
1158
|
?disabled=${unavailable}
|
|
1135
1159
|
@change=${(e: Event) => {
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1160
|
+
const typed = (e.target as HTMLInputElement).valueAsNumber
|
|
1161
|
+
this.linkCommand({ kind: 'declare-instance-size', range: rangeWith(range, axis, bound, typed), replace: true })
|
|
1138
1162
|
}}
|
|
1139
1163
|
/>`
|
|
1140
1164
|
)}
|
|
1141
1165
|
</div>
|
|
1142
1166
|
</div>`
|
|
1143
1167
|
)}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
})}
|
|
1152
|
-
</p>
|
|
1153
|
-
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })}>
|
|
1154
|
-
<md-icon>straighten</md-icon>${i18next.t('figure.button.remeasure-range')}
|
|
1155
|
-
</button>`
|
|
1156
|
-
: nothing}
|
|
1168
|
+
<p followers>
|
|
1169
|
+
${followers.length
|
|
1170
|
+
? i18next.t('figure.text.board-size-followers', {
|
|
1171
|
+
parts: followers.length > 4 ? i18next.t('figure.text.board-size-followers-more', { first: followers.slice(0, 4).join(', '), rest: String(followers.length - 4) }) : followers.join(', ')
|
|
1172
|
+
})
|
|
1173
|
+
: i18next.t('figure.text.board-size-no-followers')}
|
|
1174
|
+
</p>
|
|
1157
1175
|
${this.renderSizingRule(asset, unavailable)}
|
|
1158
1176
|
</section>`
|
|
1159
1177
|
}
|
|
@@ -1309,7 +1327,7 @@ export class FigureEditor extends LitElement {
|
|
|
1309
1327
|
this.publishing?.part === part
|
|
1310
1328
|
? this.publishing
|
|
1311
1329
|
: listed && input
|
|
1312
|
-
? { part, label: listed.label, min: input.min
|
|
1330
|
+
? /* A published input has both ends (figure-model checkPublicInput). */ { part, label: listed.label, min: input.min!, max: input.max! }
|
|
1313
1331
|
: undefined
|
|
1314
1332
|
const open = !!form
|
|
1315
1333
|
const edit = (key: 'label' | 'min' | 'max') => (e: Event) => {
|
|
@@ -1488,44 +1506,49 @@ export class FigureEditor extends LitElement {
|
|
|
1488
1506
|
this.openFastening()
|
|
1489
1507
|
}
|
|
1490
1508
|
private openFastening() {
|
|
1491
|
-
if (!this.selected || this.
|
|
1509
|
+
if (!this.selected || this.fastenPopup) return
|
|
1492
1510
|
this.fastenError = ''
|
|
1493
1511
|
this.fastenHover = null
|
|
1494
|
-
/* One panel over the side at a time: the view or the history gives way to the window. */
|
|
1495
|
-
this.overlay = undefined
|
|
1496
1512
|
this.fastening = true
|
|
1513
|
+
const content = document.createElement('figure-fasten-dialog') as NonNullable<typeof this.fastenWindow>
|
|
1514
|
+
content.addEventListener('preview-command', e => this.commandPreview.ask((e as CustomEvent).detail))
|
|
1515
|
+
content.addEventListener('fasten-hover', e => this.hoverFastening((e as CustomEvent).detail))
|
|
1516
|
+
content.addEventListener('fasten-apply', e => this.applyFastening((e as CustomEvent).detail.action))
|
|
1517
|
+
content.addEventListener('fasten-cancel', () => this.closeFastening())
|
|
1518
|
+
this.fastenWindow = content
|
|
1519
|
+
this.tellFastening()
|
|
1520
|
+
/* No backdrop: the canvas stays reachable while it is open -- a part pressed there is aimed at (ADR-0140 stage 4). */
|
|
1521
|
+
const popup = openPopup(html`${content}`, { title: i18next.t('figure.label.fastening'), size: 'fit', backdrop: false, movable: true, closable: true, escapable: true })
|
|
1522
|
+
this.fastenPopup = popup
|
|
1523
|
+
/* Closed by its own X or Esc as well: the editor lets go of it the same way. */
|
|
1524
|
+
popup.result.then(() => this.fastenPopup === popup && this.fastenClosed())
|
|
1497
1525
|
}
|
|
1498
1526
|
private closeFastening() {
|
|
1527
|
+
const popup = this.fastenPopup
|
|
1528
|
+
this.fastenClosed()
|
|
1529
|
+
popup?.close()
|
|
1530
|
+
}
|
|
1531
|
+
private fastenClosed() {
|
|
1532
|
+
this.fastenPopup = undefined
|
|
1533
|
+
this.fastenWindow = undefined
|
|
1499
1534
|
this.fastening = false
|
|
1500
1535
|
this.fastenError = ''
|
|
1501
1536
|
this.fastenHover = null
|
|
1502
1537
|
}
|
|
1503
|
-
/**
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
overlay-body
|
|
1518
|
-
.revision=${this.shown()}
|
|
1519
|
-
.partId=${this.selected}
|
|
1520
|
-
?locked=${unavailable}
|
|
1521
|
-
.previewWords=${this.commandPreview.words()}
|
|
1522
|
-
.error=${this.fastenError}
|
|
1523
|
-
@preview-command=${(e: CustomEvent) => this.commandPreview.ask(e.detail)}
|
|
1524
|
-
@fasten-hover=${(e: CustomEvent) => this.hoverFastening(e.detail)}
|
|
1525
|
-
@fasten-apply=${(e: CustomEvent) => this.applyFastening(e.detail.action)}
|
|
1526
|
-
@fasten-cancel=${() => this.closeFastening()}
|
|
1527
|
-
></figure-fasten-dialog>
|
|
1528
|
-
</div>`
|
|
1538
|
+
/** What the window shows is the editor's: the figure on screen, the part, and what the preview and a refusal said. */
|
|
1539
|
+
private tellFastening() {
|
|
1540
|
+
const content = this.fastenWindow
|
|
1541
|
+
const view = this.session?.read()
|
|
1542
|
+
if (!content || !view) return
|
|
1543
|
+
content.revision = this.shown()
|
|
1544
|
+
content.partId = this.selected
|
|
1545
|
+
content.locked = view.loading || !!view.error || view.saving || !view.context
|
|
1546
|
+
content.previewWords = this.commandPreview.words()
|
|
1547
|
+
content.error = this.fastenError
|
|
1548
|
+
}
|
|
1549
|
+
disconnectedCallback() {
|
|
1550
|
+
super.disconnectedCallback()
|
|
1551
|
+
this.closeFastening()
|
|
1529
1552
|
}
|
|
1530
1553
|
/** 「적용」: the command, one step; refused, the window stays open and says why. */
|
|
1531
1554
|
private applyFastening(action: FigureAuthoringAction) {
|
|
@@ -1584,15 +1607,16 @@ export class FigureEditor extends LitElement {
|
|
|
1584
1607
|
return this.shown()?.asset
|
|
1585
1608
|
}
|
|
1586
1609
|
/** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
|
|
1587
|
-
private renderTab(tab: SideTab, icon: string, label: string
|
|
1610
|
+
private renderTab(tab: SideTab, icon: string, label: string) {
|
|
1588
1611
|
/* Its accessible name is the name it shows (designer 2026-09-27); the longer title stays as its tooltip. */
|
|
1589
1612
|
const name = i18next.t('figure.label.tab-short-' + tab)
|
|
1590
1613
|
return html`<button tab ?on=${this.tab === tab} title=${label} aria-label=${name} @click=${() => (this.tab = tab)}>
|
|
1591
|
-
<md-icon>${icon}</md-icon><span tab-label aria-hidden="true">${name}</span
|
|
1614
|
+
<md-icon>${icon}</md-icon><span tab-label aria-hidden="true">${name}</span>
|
|
1592
1615
|
</button>`
|
|
1593
1616
|
}
|
|
1594
1617
|
updated(changed: PropertyValues<this>) {
|
|
1595
1618
|
super.updated?.(changed)
|
|
1619
|
+
this.tellFastening()
|
|
1596
1620
|
/* Nothing blocks release any more: the list under the bar closes, and opens fresh when something does again. */
|
|
1597
1621
|
if (changed.has('releaseBlockers') && !this.releaseBlockers.length && this.blockersOpen) this.blockersOpen = false
|
|
1598
1622
|
this.refusals.tell(changed)
|
|
@@ -1617,7 +1641,8 @@ export class FigureEditor extends LitElement {
|
|
|
1617
1641
|
if (!part || !drawn || part.shape.dimensions.jamb !== undefined) return nothing
|
|
1618
1642
|
const asset = this.asset() as FigureAsset
|
|
1619
1643
|
const measures = figureSectionMeasuresOf(asset, this.selected)
|
|
1620
|
-
|
|
1644
|
+
/* What a measure typed here keeps: the part's middle, or its fastening (ADR-0140 4.1). */
|
|
1645
|
+
const kept = keptBySizeOf(asset, this.selected)
|
|
1621
1646
|
return html`<figure-section-editor
|
|
1622
1647
|
.body=${{ ...part, id: this.selected }}
|
|
1623
1648
|
.measures=${measures}
|
|
@@ -1765,13 +1790,12 @@ export class FigureEditor extends LitElement {
|
|
|
1765
1790
|
held = undefined
|
|
1766
1791
|
}
|
|
1767
1792
|
const roomFields = own.filter(i => roomOnly.includes(i.id))
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
: 'figure.text.saved'
|
|
1793
|
+
/*
|
|
1794
|
+
Only what is under way is said. Saved or not is the save button's: filled and pressable when there is something
|
|
1795
|
+
to save, dimmed when not -- the saved and unsaved words beside it said the same thing again in the primary
|
|
1796
|
+
colour, read as a button (screen copy rule: a state is the control's).
|
|
1797
|
+
*/
|
|
1798
|
+
const status = view.loading ? 'figure.text.loading-figure' : view.saving ? 'figure.text.saving-figure' : ''
|
|
1775
1799
|
/*
|
|
1776
1800
|
A figure with no parts yet is not one that failed: what blocks its release is said once it has something to
|
|
1777
1801
|
release (first 15 minutes, 2026-10-16 evening -- red from the first click read as a failure). The verdict tab
|
|
@@ -1799,7 +1823,7 @@ export class FigureEditor extends LitElement {
|
|
|
1799
1823
|
${i18next.t('figure.label.mode-' + mode)}
|
|
1800
1824
|
</button>`
|
|
1801
1825
|
)}
|
|
1802
|
-
|
|
1826
|
+
${status ? html`<span slot="status" class="status" role="status">${i18next.t(status)}</span>` : nothing}
|
|
1803
1827
|
<span slot="status" release-state>${this.releaseWords(view)}</span>
|
|
1804
1828
|
${blockers.length
|
|
1805
1829
|
? html`<button slot="commit" release-blocked aria-expanded=${String(this.blockersOpen)} @click=${() => (this.blockersOpen = !this.blockersOpen)}>
|
|
@@ -1882,7 +1906,6 @@ export class FigureEditor extends LitElement {
|
|
|
1882
1906
|
<figure-canvas
|
|
1883
1907
|
.view=${this.view}
|
|
1884
1908
|
.parts=${this.drawnParts(view.geometry, live)}
|
|
1885
|
-
.anchors=${this.anchorsOf(asset)}
|
|
1886
1909
|
.fan=${this.fanOf(shown, live)}
|
|
1887
1910
|
.ghost=${this.previewShown().ghost}
|
|
1888
1911
|
.highlight=${this.previewShown().highlight}
|
|
@@ -1890,13 +1913,17 @@ export class FigureEditor extends LitElement {
|
|
|
1890
1913
|
.palette=${view.palette ?? {}}
|
|
1891
1914
|
.context=${view.context ?? ''}
|
|
1892
1915
|
.selected=${this.selected}
|
|
1916
|
+
?aiming=${this.fastening}
|
|
1893
1917
|
.reach=${unavailable ? { translate: false, rotate: false, scale: false } : this.reachOf(this.selected)}
|
|
1918
|
+
@part-sizing=${(e: CustomEvent) => this.resizing(e.detail.id, e.detail.result)}
|
|
1894
1919
|
@part-transformed=${(e: CustomEvent) => this.transformed(e.detail.id, e.detail.result)}
|
|
1895
1920
|
@part-selected=${(e: CustomEvent<string>) => this.choose(e.detail)}
|
|
1921
|
+
@part-aimed=${(e: CustomEvent<{ id: string; point?: number[]; normal?: number[] }>) =>
|
|
1922
|
+
this.fastenWindow?.aimAt?.(e.detail)}
|
|
1896
1923
|
@render-error=${(e: CustomEvent<Error>) => {
|
|
1897
1924
|
this.error = e.detail.message
|
|
1898
1925
|
}}
|
|
1899
|
-
></figure-canvas
|
|
1926
|
+
></figure-canvas>${this.renderSameMeasure(unavailable)}</div>
|
|
1900
1927
|
<aside slot="end">
|
|
1901
1928
|
<div tabs>
|
|
1902
1929
|
${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
|
|
@@ -1904,7 +1931,7 @@ export class FigureEditor extends LitElement {
|
|
|
1904
1931
|
${this.renderTab('motion', 'animation', i18next.t('figure.label.motion'))}
|
|
1905
1932
|
${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}
|
|
1906
1933
|
${this.renderTab('data', 'sensors', i18next.t('figure.label.tab-data'))}
|
|
1907
|
-
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity')
|
|
1934
|
+
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'))}
|
|
1908
1935
|
</div>
|
|
1909
1936
|
<slot name="notice"></slot>
|
|
1910
1937
|
<div panel ?off=${tab !== 'properties'}>
|
|
@@ -1980,7 +2007,7 @@ export class FigureEditor extends LitElement {
|
|
|
1980
2007
|
<div panel flush ?off=${tab !== 'verdict'}><slot name="verdict"></slot></div>
|
|
1981
2008
|
</aside>
|
|
1982
2009
|
</ox-three-pane>
|
|
1983
|
-
${this.renderOverlay()}
|
|
2010
|
+
${this.renderOverlay()}`
|
|
1984
2011
|
}
|
|
1985
2012
|
/** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
|
|
1986
2013
|
private renderGroups(
|
|
@@ -2057,6 +2084,12 @@ export class FigureEditor extends LitElement {
|
|
|
2057
2084
|
* rule" named nothing): a fastened part's place and turn are its fastening's; other numbers are worked out from
|
|
2058
2085
|
* something else. A size that follows something is said at the panel's head, so not again here.
|
|
2059
2086
|
*/
|
|
2087
|
+
/** What a size typed keeps, under the size fields: the part's middle, where it is fastened, or only its size (ADR-0140 4.1). */
|
|
2088
|
+
private renderSizeKeeps() {
|
|
2089
|
+
const asset = this.asset()
|
|
2090
|
+
const kept = asset && this.selected ? keptBySizeOf(asset, this.selected) : undefined
|
|
2091
|
+
return kept ? html`<p hint size-keeps>${i18next.t(SIZE_KEEPS_WORDS[kept])}</p>` : nothing
|
|
2092
|
+
}
|
|
2060
2093
|
private renderHeldAxes(vectors: FieldGroup[]) {
|
|
2061
2094
|
const fastened = !!(this.asset())?.joints?.some(j => j.body1.part === ownerOf(this.selected))
|
|
2062
2095
|
const held = vectors.filter(g => g.kind === 'vector' && g.name !== 'size' && (['x', 'y', 'z'] as const).some(axis => !g.cells[axis]))
|