@things-factory/figure-ui 10.1.203 → 10.1.205
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 +7 -22
- package/client/modeller/editor.ts +176 -145
- package/client/modeller/fasten-form.ts +91 -9
- package/client/modeller/figure-fasten-dialog.ts +42 -9
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +12 -1
- package/client/modeller/links.ts +48 -6
- 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 +7 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +28 -32
- package/dist-client/modeller/editor.js +176 -153
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +36 -3
- package/dist-client/modeller/fasten-form.js +86 -8
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +17 -3
- package/dist-client/modeller/figure-fasten-dialog.js +47 -10
- 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-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 +12 -1
- package/dist-client/modeller/gizmo.js +8 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/links.d.ts +14 -2
- package/dist-client/modeller/links.js +42 -4
- package/dist-client/modeller/links.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/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 +10 -0
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +94 -56
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +32 -10
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +2 -2
- package/test/fasten-form.test.ts +82 -2
- package/test/gizmo.test.ts +13 -15
- package/test/links.test.ts +8 -3
- 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 +25 -22
- package/translations/es.json +25 -22
- package/translations/ja.json +25 -22
- package/translations/ko.json +25 -22
- package/translations/ms.json +25 -22
- package/translations/zh.json +25 -22
|
@@ -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,7 +30,8 @@ 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, 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'
|
|
@@ -52,7 +53,7 @@ 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 }
|
|
@@ -62,7 +63,8 @@ import type { FigureGateFinding } from '../types.js'
|
|
|
62
63
|
import { editRefusalOf, inDeclaredRoom, narrowingRetryOf, partNameRefusalOf, 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, instanceRangeToDeclare, proportionToFollow, rangeDiffers, 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
|
/**
|
|
@@ -139,12 +141,21 @@ export class FigureEditor extends LitElement {
|
|
|
139
141
|
/** The view settings or the release history, open over the work from the bar. */
|
|
140
142
|
@state() private overlay?: BarOverlay
|
|
141
143
|
/**
|
|
142
|
-
* The fastening window open for the selected part (user 2026-10-17)
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
144
|
+
* The fastening window open for the selected part (user 2026-10-17), in the standard popup, moved by its head
|
|
145
|
+
* (2026-10-18; put back 2026-10-20 -- the side panel it was moved to could not be moved), with no backdrop, so the
|
|
146
|
+
* canvas can be pressed while it is open: chosen there, applied once. It closes with nothing written when the part,
|
|
147
|
+
* the tab or the middle changes under it.
|
|
146
148
|
*/
|
|
147
149
|
@state() private fastening = false
|
|
150
|
+
private fastenPopup?: { close: () => void }
|
|
151
|
+
private fastenWindow?: HTMLElement & {
|
|
152
|
+
revision?: FigureRevision
|
|
153
|
+
partId: string
|
|
154
|
+
locked: boolean
|
|
155
|
+
previewWords: string[]
|
|
156
|
+
error: string
|
|
157
|
+
aimAt?: (at: { id: string; point?: number[]; normal?: number[] }) => void
|
|
158
|
+
}
|
|
148
159
|
/** Why the window's 「적용」 was refused, said in it. */
|
|
149
160
|
@state() private fastenError = ''
|
|
150
161
|
/** 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 +216,8 @@ export class FigureEditor extends LitElement {
|
|
|
205
216
|
@state() private hiddenParts = new Set<string>()
|
|
206
217
|
/** A value in hand -- typed, held on a spinner, scrubbed, dragged -- drawn at once and recorded on release. */
|
|
207
218
|
@state() private live: LiveValues = NOTHING_LIVE
|
|
219
|
+
/** A size drag in hand, drawn from figure-model each frame (ADR-0140 4.4); gone with the release. */
|
|
220
|
+
@state() private sizing?: { part: string; drag: SizeDrag; frame?: SizeDragFrame; through?: number }
|
|
208
221
|
/** The part whose drawn section is open in the section editor (ADR-0100 §3.2), or null. */
|
|
209
222
|
@state() private sectionEditing: string | null = null
|
|
210
223
|
private lastContext: string | null = null
|
|
@@ -234,7 +247,7 @@ export class FigureEditor extends LitElement {
|
|
|
234
247
|
aria-pressed=${String(this.gizmoMode === tool.mode)}
|
|
235
248
|
?disabled=${!tool.can}
|
|
236
249
|
aria-label=${i18next.t('figure.button.gizmo-' + tool.mode)}
|
|
237
|
-
title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' +
|
|
250
|
+
title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + i18next.t('figure.text.gizmo-not-own')}`}
|
|
238
251
|
@click=${() => this.useGizmo(tool.mode)}
|
|
239
252
|
>
|
|
240
253
|
<md-icon>${tool.icon}</md-icon>
|
|
@@ -292,7 +305,7 @@ export class FigureEditor extends LitElement {
|
|
|
292
305
|
if (view && !view.geometry.some(g => ownerOf(g.id) === this.selected)) this.selected = ''
|
|
293
306
|
if (_changed.has('selected')) this.gizmoMode = undefined
|
|
294
307
|
/* 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'
|
|
308
|
+
if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit')) this.closeFastening()
|
|
296
309
|
/*
|
|
297
310
|
* The section editor is of one part: when the selection moves off it by any way -- another part picked, the scene's
|
|
298
311
|
* own Esc, the part removed, another figure opened -- it closes and lets go of the preview it held.
|
|
@@ -322,13 +335,10 @@ export class FigureEditor extends LitElement {
|
|
|
322
335
|
private useGizmo(mode: 'translate' | 'rotate' | 'scale') {
|
|
323
336
|
/*
|
|
324
337
|
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.
|
|
338
|
+
put on it to drag in vain (:3300, 10.1.196) -- the tool's own words are said instead.
|
|
327
339
|
*/
|
|
328
340
|
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'))
|
|
341
|
+
this.refusals.say(i18next.t('figure.text.gizmo-not-own'))
|
|
332
342
|
return
|
|
333
343
|
}
|
|
334
344
|
this.renderRoot.querySelector<FigureCanvas>('figure-canvas')?.gizmo(mode)
|
|
@@ -354,6 +364,7 @@ export class FigureEditor extends LitElement {
|
|
|
354
364
|
} finally {
|
|
355
365
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
356
366
|
this.live = NOTHING_LIVE
|
|
367
|
+
this.sizing = undefined
|
|
357
368
|
}
|
|
358
369
|
}
|
|
359
370
|
/** The bodies drawn with the value in hand, or null when nothing is in hand (or the figure cannot take it). */
|
|
@@ -363,7 +374,8 @@ export class FigureEditor extends LitElement {
|
|
|
363
374
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
364
375
|
private drawnParts(geometry: Parameters<typeof drawableFigureParts>[0] | undefined, live: FigureRenderPart[] | null) {
|
|
365
376
|
const parts = live ?? drawableFigureParts(geometry ?? [])
|
|
366
|
-
|
|
377
|
+
const frame = this.sizing?.frame
|
|
378
|
+
return (frame ? partsAtFrame(parts, frame.moved) : parts).filter(body => !this.hiddenParts.has(ownerOf((body as { id: string }).id)))
|
|
367
379
|
}
|
|
368
380
|
/**
|
|
369
381
|
* Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
|
|
@@ -563,60 +575,42 @@ export class FigureEditor extends LitElement {
|
|
|
563
575
|
if (!id || !this.session || !this.session.read().geometry.some(g => g.id === id)) return none
|
|
564
576
|
try {
|
|
565
577
|
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
|
|
578
|
+
/* 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). */
|
|
579
|
+
return gizmoReachOf(asset, id)
|
|
574
580
|
} catch {
|
|
575
581
|
return none
|
|
576
582
|
}
|
|
577
583
|
}
|
|
578
584
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
* 2026-10-19, the fan's blade-2). Its tool says it is placed by something else, as before.
|
|
585
|
+
* A size drag in hand, a frame of it: the sizes the handle comes to, through figure-model's size function and the
|
|
586
|
+
* evaluator stepped to them, drawn and written nowhere (ADR-0140 4.4). Begun at the first frame, from the figure as it
|
|
587
|
+
* is drawn and the handle the scene says was held; the release writes the last frame's changes.
|
|
583
588
|
*/
|
|
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 {
|
|
589
|
+
private resizing(id: string, result: GizmoResult) {
|
|
590
|
+
const part = ownerOf(id)
|
|
591
|
+
const shown = this.shown()
|
|
592
|
+
if (!shown || !result.scale) return
|
|
593
593
|
try {
|
|
594
|
-
|
|
594
|
+
const drawn = drawableFigureParts(this.session!.read().geometry ?? [])
|
|
595
|
+
if (this.sizing?.part !== part)
|
|
596
|
+
this.sizing = { part, drag: startSizeDrag(shown.asset, part, shown.inputs, sizeKeepOf(result.grab)), through: throughPlaneOf(shown.asset, drawn, part) }
|
|
597
|
+
const sizes = gizmoChanges(id, this.reachOf(id), { scale: result.scale }, { ...shown.inputs })
|
|
598
|
+
this.sizing = { ...this.sizing, frame: this.sizing.drag.frame(sizes) }
|
|
595
599
|
} catch {
|
|
596
|
-
|
|
600
|
+
/* A size the figure cannot take on the way: the figure as it stands is drawn, and the release says why. */
|
|
601
|
+
this.sizing = undefined
|
|
597
602
|
}
|
|
598
603
|
}
|
|
599
604
|
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
605
|
+
* A free part a size change takes past the mounting plane: said, with 「…에 붙이기」 to fasten it there (ADR-0140 5). The
|
|
606
|
+
* size rule is not changed for it: what the author picks is the fastening, one step of the history.
|
|
602
607
|
*/
|
|
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()
|
|
608
|
+
private tellThroughPlane(part: string, mm: number) {
|
|
609
|
+
const plane = i18next.t('figure.label.resize-place-' + (this.asset()?.placement ?? 'floor'))
|
|
610
|
+
this.refusals.point(i18next.t('figure.text.size-through-plane', { part: partLabelOf(part), plane, mm: String(mm) }), {
|
|
611
|
+
label: i18next.t('figure.button.fasten-to-plane', { plane }),
|
|
612
|
+
run: () => this.run(() => this.session!.command({ kind: 'attach', part, to: { plane: 'mounting-plane' } }))
|
|
613
|
+
})
|
|
620
614
|
}
|
|
621
615
|
/** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
|
|
622
616
|
private transformed(id: string, result: GizmoResult) {
|
|
@@ -642,21 +636,21 @@ export class FigureEditor extends LitElement {
|
|
|
642
636
|
the shift had been taken for a move of the joint, and the size was dropped).
|
|
643
637
|
*/
|
|
644
638
|
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
639
|
/*
|
|
651
|
-
A size dragged
|
|
652
|
-
|
|
640
|
+
A size dragged is written as its last frame was drawn: figure-model's one size function, keeping the side opposite
|
|
641
|
+
the handle held -- or, fastened, its fastening (ADR-0140 4.1 · 4.4). What the scene put the part at is not read.
|
|
653
642
|
*/
|
|
654
643
|
const scaled = !!result.scale && !result.rotation && result.scale.some(k => Math.abs(k - 1) > 1e-4)
|
|
655
644
|
const taken = jointed || scaled ? { scale: result.scale } : result
|
|
656
645
|
const written = gizmoChanges(id, this.reachOf(id), taken, sizes)
|
|
657
|
-
const
|
|
646
|
+
const drawnLast = this.sizing?.part === ownerOf(id) ? this.sizing.frame : undefined
|
|
647
|
+
const changes = !scaled ? written : drawnLast ? drawnLast.changes : figureSizeEditOf(asset, ownerOf(id), written, sizeKeepOf(result.grab)).changes
|
|
658
648
|
if (changes.length) {
|
|
649
|
+
const through = scaled ? (this.sizing?.part === ownerOf(id) ? this.sizing.through : undefined) ?? throughPlaneOf(asset, drawableFigureParts(view.geometry), ownerOf(id)) : 0
|
|
659
650
|
this.run(() => this.session!.apply(changes))
|
|
651
|
+
/* 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). */
|
|
652
|
+
const now = scaled ? throughPlaneOf(this.shown()!.asset, drawableFigureParts(this.session!.read().geometry ?? []), ownerOf(id)) : 0
|
|
653
|
+
if (now > through + 0.5) this.tellThroughPlane(ownerOf(id), now)
|
|
660
654
|
return
|
|
661
655
|
}
|
|
662
656
|
if (jointed && result.scale) {
|
|
@@ -850,6 +844,7 @@ export class FigureEditor extends LitElement {
|
|
|
850
844
|
${groups.singles.length
|
|
851
845
|
? html`<div form>${groups.singles.map(g => (g.kind === 'single' ? this.renderSingle(g.name, g.field, unavailable) : nothing))}</div>`
|
|
852
846
|
: nothing}
|
|
847
|
+
${this.renderSizeKeeps()}
|
|
853
848
|
</div>`
|
|
854
849
|
: nothing}
|
|
855
850
|
${place.length && !placeLocked
|
|
@@ -934,24 +929,6 @@ export class FigureEditor extends LitElement {
|
|
|
934
929
|
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
930
|
${panel('history', 'figure.button.release', html`<slot name="history"></slot>`)}`
|
|
936
931
|
}
|
|
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
932
|
/** Data tried in the data tab, given to the preview's figures as their `data`; null when none is (ADR-0051 §8). */
|
|
956
933
|
@state() private dataSample: unknown = null
|
|
957
934
|
/** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
|
|
@@ -1037,17 +1014,38 @@ export class FigureEditor extends LitElement {
|
|
|
1037
1014
|
private changeField(field: { id: string; unit: FigureInputChange['unit']; role?: string }, event: Event) {
|
|
1038
1015
|
const input = event.target as HTMLInputElement,
|
|
1039
1016
|
value = input.valueAsNumber
|
|
1040
|
-
/* A
|
|
1017
|
+
/* A number typed keeps the part's middle, or its fastening -- figure-model's one size function (ADR-0140 4.1). */
|
|
1041
1018
|
const part = this.selected
|
|
1042
1019
|
const change: FigureInputChange = { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value }
|
|
1043
|
-
|
|
1020
|
+
const asset = this.shown()?.asset
|
|
1021
|
+
const before = asset && part ? throughPlaneOf(asset, drawableFigureParts(this.session!.read().geometry ?? []), part) : 0
|
|
1022
|
+
this.run(() => this.session!.apply(field.role !== 'state' && part ? figureSizeEditOf(this.shown()!.asset, part, [change], { keep: 'centre' }).changes : [change]))
|
|
1023
|
+
/* Typed past the mounting plane: said, with the way to fasten it there (ADR-0140 5). */
|
|
1024
|
+
const after = asset && part && field.role !== 'state' ? throughPlaneOf(this.shown()!.asset, drawableFigureParts(this.session!.read().geometry ?? []), part) : 0
|
|
1025
|
+
if (after > before + 0.5) this.tellThroughPlane(part, after)
|
|
1044
1026
|
// Rejected edits must also restore the displayed numeric value.
|
|
1045
1027
|
input.value = String(this.session!.read().fields.find(i => i.id === field.id)!.value)
|
|
1046
1028
|
}
|
|
1047
1029
|
/** 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) {
|
|
1030
|
+
private typing(field: { id: string; unit?: FigureInputChange['unit']; role?: string }, event: Event) {
|
|
1049
1031
|
const value = (event.target as HTMLInputElement).valueAsNumber
|
|
1050
|
-
if (Number.isFinite(value)) this.hold(
|
|
1032
|
+
if (Number.isFinite(value)) this.hold(this.heldOf(field, value))
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* The values in hand for a number typed or scrubbed: what its release writes -- a size with the part's middle kept,
|
|
1036
|
+
* or its fastening (figure-model's one size function, ADR-0140 4.1), so the figure does not jump on the release.
|
|
1037
|
+
*/
|
|
1038
|
+
private heldOf(field: { id: string; unit?: FigureInputChange['unit']; role?: string }, value: number, sized?: ReturnType<typeof figureSizeEditorOf>): Record<string, number> {
|
|
1039
|
+
const part = this.selected
|
|
1040
|
+
const asset = this.shown()?.asset
|
|
1041
|
+
if (field.role === 'state' || !part || !asset) return { [field.id]: value }
|
|
1042
|
+
const change: FigureInputChange = { kind: 'set-design', input: field.id, unit: field.unit ?? 'mm', value }
|
|
1043
|
+
try {
|
|
1044
|
+
const { changes } = sized ? sized([change], { keep: 'centre' }) : figureSizeEditOf(asset, part, [change], { keep: 'centre' })
|
|
1045
|
+
return Object.fromEntries(changes.map(c => [c.input, c.value as number]))
|
|
1046
|
+
} catch {
|
|
1047
|
+
return { [field.id]: value }
|
|
1048
|
+
}
|
|
1051
1049
|
}
|
|
1052
1050
|
/**
|
|
1053
1051
|
* Drag a field's label (or an axis letter) sideways to scrub its value, as vector tools do: the figure follows the
|
|
@@ -1062,10 +1060,13 @@ export class FigureEditor extends LitElement {
|
|
|
1062
1060
|
label.setPointerCapture(down.pointerId)
|
|
1063
1061
|
const start = field.value
|
|
1064
1062
|
let value = start
|
|
1063
|
+
/* Prepared once for the scrub: each move is a size through the same function its release writes with. */
|
|
1064
|
+
const asset = this.shown()?.asset
|
|
1065
|
+
const sized = asset && this.selected && field.role !== 'state' ? (() => { try { return figureSizeEditorOf(asset, this.selected) } catch { return undefined } })() : undefined
|
|
1065
1066
|
const move = (e: PointerEvent) => {
|
|
1066
1067
|
value = scrubbed(start, e.clientX - down.clientX, scrubStep(field.unit, start, e.shiftKey))
|
|
1067
1068
|
input.value = String(value)
|
|
1068
|
-
this.hold(
|
|
1069
|
+
this.hold(this.heldOf(field, value, sized))
|
|
1069
1070
|
}
|
|
1070
1071
|
const up = (e: PointerEvent) => {
|
|
1071
1072
|
label.removeEventListener('pointermove', move)
|
|
@@ -1106,27 +1107,35 @@ export class FigureEditor extends LitElement {
|
|
|
1106
1107
|
return { of, toDeclare }
|
|
1107
1108
|
})
|
|
1108
1109
|
const range = measured.of
|
|
1109
|
-
/*
|
|
1110
|
+
/*
|
|
1111
|
+
In words a person reads (user 2026-10-19/20): what the figure does on a drawing, not the inputs behind it. Fixed,
|
|
1112
|
+
it says the size it is always drawn at; resizable, the sizes it may take on each axis and the parts that change
|
|
1113
|
+
with it. The way to it stays one press -- a figure no part follows (a robot scaled whole) reaches the one-ratio
|
|
1114
|
+
rule only from here -- and the help mark says how parts are made to follow.
|
|
1115
|
+
*/
|
|
1116
|
+
const mm = (v: number) => Math.round(v).toLocaleString()
|
|
1110
1117
|
const toDeclare = measured.toDeclare
|
|
1118
|
+
const title = (explained: string) =>
|
|
1119
|
+
html`<h3>${i18next.t('figure.label.board-size')}${this.help.button(explained)}</h3>${this.help.hint(explained)}`
|
|
1111
1120
|
if (!range && toDeclare)
|
|
1112
|
-
return html`<section>
|
|
1113
|
-
|
|
1114
|
-
<p
|
|
1121
|
+
return html`<section board-size>
|
|
1122
|
+
${title('figure.text.board-size-fixed-explained')}
|
|
1123
|
+
<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>
|
|
1115
1124
|
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: toDeclare.range, at: toDeclare.at })}>
|
|
1116
|
-
<md-icon>
|
|
1125
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.declare-instance-size')}
|
|
1117
1126
|
</button>
|
|
1118
1127
|
</section>`
|
|
1119
1128
|
if (!range) return nothing
|
|
1120
|
-
const mm = (v: number) => Math.round(v).toLocaleString()
|
|
1121
1129
|
const differs = rangeDiffers(range.declared, range.measured)
|
|
1122
|
-
|
|
1123
|
-
|
|
1130
|
+
const followers = sizeFollowersOf(asset)
|
|
1131
|
+
return html`<section board-size>
|
|
1132
|
+
${title('figure.text.board-size-range-explained')}
|
|
1124
1133
|
${(['x', 'y', 'z'] as const).map(
|
|
1125
1134
|
axis => html`<div inline-field>
|
|
1126
|
-
<label>${
|
|
1127
|
-
<div pair>
|
|
1135
|
+
<label>${i18next.t('figure.label.resize-' + axis)} <span unit>mm</span></label>
|
|
1136
|
+
<div pair range>
|
|
1128
1137
|
${(['min', 'max'] as const).map(
|
|
1129
|
-
bound => html`<input
|
|
1138
|
+
(bound, i) => html`${i ? html`<span dash aria-hidden="true">–</span>` : nothing}<input
|
|
1130
1139
|
aria-label=${`size.${axis}.${bound}`}
|
|
1131
1140
|
type="number"
|
|
1132
1141
|
step="any"
|
|
@@ -1141,6 +1150,13 @@ export class FigureEditor extends LitElement {
|
|
|
1141
1150
|
</div>
|
|
1142
1151
|
</div>`
|
|
1143
1152
|
)}
|
|
1153
|
+
<p followers>
|
|
1154
|
+
${followers.length
|
|
1155
|
+
? i18next.t('figure.text.board-size-followers', {
|
|
1156
|
+
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(', ')
|
|
1157
|
+
})
|
|
1158
|
+
: i18next.t('figure.text.board-size-no-followers')}
|
|
1159
|
+
</p>
|
|
1144
1160
|
${differs
|
|
1145
1161
|
? html`<p hint>
|
|
1146
1162
|
${i18next.t('figure.text.instance-range-can-widen', {
|
|
@@ -1488,44 +1504,49 @@ export class FigureEditor extends LitElement {
|
|
|
1488
1504
|
this.openFastening()
|
|
1489
1505
|
}
|
|
1490
1506
|
private openFastening() {
|
|
1491
|
-
if (!this.selected || this.
|
|
1507
|
+
if (!this.selected || this.fastenPopup) return
|
|
1492
1508
|
this.fastenError = ''
|
|
1493
1509
|
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
1510
|
this.fastening = true
|
|
1511
|
+
const content = document.createElement('figure-fasten-dialog') as NonNullable<typeof this.fastenWindow>
|
|
1512
|
+
content.addEventListener('preview-command', e => this.commandPreview.ask((e as CustomEvent).detail))
|
|
1513
|
+
content.addEventListener('fasten-hover', e => this.hoverFastening((e as CustomEvent).detail))
|
|
1514
|
+
content.addEventListener('fasten-apply', e => this.applyFastening((e as CustomEvent).detail.action))
|
|
1515
|
+
content.addEventListener('fasten-cancel', () => this.closeFastening())
|
|
1516
|
+
this.fastenWindow = content
|
|
1517
|
+
this.tellFastening()
|
|
1518
|
+
/* No backdrop: the canvas stays reachable while it is open -- a part pressed there is aimed at (ADR-0140 stage 4). */
|
|
1519
|
+
const popup = openPopup(html`${content}`, { title: i18next.t('figure.label.fastening'), size: 'fit', backdrop: false, movable: true, closable: true, escapable: true })
|
|
1520
|
+
this.fastenPopup = popup
|
|
1521
|
+
/* Closed by its own X or Esc as well: the editor lets go of it the same way. */
|
|
1522
|
+
popup.result.then(() => this.fastenPopup === popup && this.fastenClosed())
|
|
1497
1523
|
}
|
|
1498
1524
|
private closeFastening() {
|
|
1525
|
+
const popup = this.fastenPopup
|
|
1526
|
+
this.fastenClosed()
|
|
1527
|
+
popup?.close()
|
|
1528
|
+
}
|
|
1529
|
+
private fastenClosed() {
|
|
1530
|
+
this.fastenPopup = undefined
|
|
1531
|
+
this.fastenWindow = undefined
|
|
1499
1532
|
this.fastening = false
|
|
1500
1533
|
this.fastenError = ''
|
|
1501
1534
|
this.fastenHover = null
|
|
1502
1535
|
}
|
|
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>`
|
|
1536
|
+
/** What the window shows is the editor's: the figure on screen, the part, and what the preview and a refusal said. */
|
|
1537
|
+
private tellFastening() {
|
|
1538
|
+
const content = this.fastenWindow
|
|
1539
|
+
const view = this.session?.read()
|
|
1540
|
+
if (!content || !view) return
|
|
1541
|
+
content.revision = this.shown()
|
|
1542
|
+
content.partId = this.selected
|
|
1543
|
+
content.locked = view.loading || !!view.error || view.saving || !view.context
|
|
1544
|
+
content.previewWords = this.commandPreview.words()
|
|
1545
|
+
content.error = this.fastenError
|
|
1546
|
+
}
|
|
1547
|
+
disconnectedCallback() {
|
|
1548
|
+
super.disconnectedCallback()
|
|
1549
|
+
this.closeFastening()
|
|
1529
1550
|
}
|
|
1530
1551
|
/** 「적용」: the command, one step; refused, the window stays open and says why. */
|
|
1531
1552
|
private applyFastening(action: FigureAuthoringAction) {
|
|
@@ -1584,15 +1605,16 @@ export class FigureEditor extends LitElement {
|
|
|
1584
1605
|
return this.shown()?.asset
|
|
1585
1606
|
}
|
|
1586
1607
|
/** 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
|
|
1608
|
+
private renderTab(tab: SideTab, icon: string, label: string) {
|
|
1588
1609
|
/* Its accessible name is the name it shows (designer 2026-09-27); the longer title stays as its tooltip. */
|
|
1589
1610
|
const name = i18next.t('figure.label.tab-short-' + tab)
|
|
1590
1611
|
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
|
|
1612
|
+
<md-icon>${icon}</md-icon><span tab-label aria-hidden="true">${name}</span>
|
|
1592
1613
|
</button>`
|
|
1593
1614
|
}
|
|
1594
1615
|
updated(changed: PropertyValues<this>) {
|
|
1595
1616
|
super.updated?.(changed)
|
|
1617
|
+
this.tellFastening()
|
|
1596
1618
|
/* Nothing blocks release any more: the list under the bar closes, and opens fresh when something does again. */
|
|
1597
1619
|
if (changed.has('releaseBlockers') && !this.releaseBlockers.length && this.blockersOpen) this.blockersOpen = false
|
|
1598
1620
|
this.refusals.tell(changed)
|
|
@@ -1617,7 +1639,8 @@ export class FigureEditor extends LitElement {
|
|
|
1617
1639
|
if (!part || !drawn || part.shape.dimensions.jamb !== undefined) return nothing
|
|
1618
1640
|
const asset = this.asset() as FigureAsset
|
|
1619
1641
|
const measures = figureSectionMeasuresOf(asset, this.selected)
|
|
1620
|
-
|
|
1642
|
+
/* What a measure typed here keeps: the part's middle, or its fastening (ADR-0140 4.1). */
|
|
1643
|
+
const kept = keptBySizeOf(asset, this.selected)
|
|
1621
1644
|
return html`<figure-section-editor
|
|
1622
1645
|
.body=${{ ...part, id: this.selected }}
|
|
1623
1646
|
.measures=${measures}
|
|
@@ -1765,13 +1788,12 @@ export class FigureEditor extends LitElement {
|
|
|
1765
1788
|
held = undefined
|
|
1766
1789
|
}
|
|
1767
1790
|
const roomFields = own.filter(i => roomOnly.includes(i.id))
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
: 'figure.text.saved'
|
|
1791
|
+
/*
|
|
1792
|
+
Only what is under way is said. Saved or not is the save button's: filled and pressable when there is something
|
|
1793
|
+
to save, dimmed when not -- the saved and unsaved words beside it said the same thing again in the primary
|
|
1794
|
+
colour, read as a button (screen copy rule: a state is the control's).
|
|
1795
|
+
*/
|
|
1796
|
+
const status = view.loading ? 'figure.text.loading-figure' : view.saving ? 'figure.text.saving-figure' : ''
|
|
1775
1797
|
/*
|
|
1776
1798
|
A figure with no parts yet is not one that failed: what blocks its release is said once it has something to
|
|
1777
1799
|
release (first 15 minutes, 2026-10-16 evening -- red from the first click read as a failure). The verdict tab
|
|
@@ -1799,7 +1821,7 @@ export class FigureEditor extends LitElement {
|
|
|
1799
1821
|
${i18next.t('figure.label.mode-' + mode)}
|
|
1800
1822
|
</button>`
|
|
1801
1823
|
)}
|
|
1802
|
-
|
|
1824
|
+
${status ? html`<span slot="status" class="status" role="status">${i18next.t(status)}</span>` : nothing}
|
|
1803
1825
|
<span slot="status" release-state>${this.releaseWords(view)}</span>
|
|
1804
1826
|
${blockers.length
|
|
1805
1827
|
? html`<button slot="commit" release-blocked aria-expanded=${String(this.blockersOpen)} @click=${() => (this.blockersOpen = !this.blockersOpen)}>
|
|
@@ -1882,7 +1904,6 @@ export class FigureEditor extends LitElement {
|
|
|
1882
1904
|
<figure-canvas
|
|
1883
1905
|
.view=${this.view}
|
|
1884
1906
|
.parts=${this.drawnParts(view.geometry, live)}
|
|
1885
|
-
.anchors=${this.anchorsOf(asset)}
|
|
1886
1907
|
.fan=${this.fanOf(shown, live)}
|
|
1887
1908
|
.ghost=${this.previewShown().ghost}
|
|
1888
1909
|
.highlight=${this.previewShown().highlight}
|
|
@@ -1890,9 +1911,13 @@ export class FigureEditor extends LitElement {
|
|
|
1890
1911
|
.palette=${view.palette ?? {}}
|
|
1891
1912
|
.context=${view.context ?? ''}
|
|
1892
1913
|
.selected=${this.selected}
|
|
1914
|
+
?aiming=${this.fastening}
|
|
1893
1915
|
.reach=${unavailable ? { translate: false, rotate: false, scale: false } : this.reachOf(this.selected)}
|
|
1916
|
+
@part-sizing=${(e: CustomEvent) => this.resizing(e.detail.id, e.detail.result)}
|
|
1894
1917
|
@part-transformed=${(e: CustomEvent) => this.transformed(e.detail.id, e.detail.result)}
|
|
1895
1918
|
@part-selected=${(e: CustomEvent<string>) => this.choose(e.detail)}
|
|
1919
|
+
@part-aimed=${(e: CustomEvent<{ id: string; point?: number[]; normal?: number[] }>) =>
|
|
1920
|
+
this.fastenWindow?.aimAt?.(e.detail)}
|
|
1896
1921
|
@render-error=${(e: CustomEvent<Error>) => {
|
|
1897
1922
|
this.error = e.detail.message
|
|
1898
1923
|
}}
|
|
@@ -1904,7 +1929,7 @@ export class FigureEditor extends LitElement {
|
|
|
1904
1929
|
${this.renderTab('motion', 'animation', i18next.t('figure.label.motion'))}
|
|
1905
1930
|
${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}
|
|
1906
1931
|
${this.renderTab('data', 'sensors', i18next.t('figure.label.tab-data'))}
|
|
1907
|
-
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity')
|
|
1932
|
+
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'))}
|
|
1908
1933
|
</div>
|
|
1909
1934
|
<slot name="notice"></slot>
|
|
1910
1935
|
<div panel ?off=${tab !== 'properties'}>
|
|
@@ -1980,7 +2005,7 @@ export class FigureEditor extends LitElement {
|
|
|
1980
2005
|
<div panel flush ?off=${tab !== 'verdict'}><slot name="verdict"></slot></div>
|
|
1981
2006
|
</aside>
|
|
1982
2007
|
</ox-three-pane>
|
|
1983
|
-
${this.renderOverlay()}
|
|
2008
|
+
${this.renderOverlay()}`
|
|
1984
2009
|
}
|
|
1985
2010
|
/** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
|
|
1986
2011
|
private renderGroups(
|
|
@@ -2057,6 +2082,12 @@ export class FigureEditor extends LitElement {
|
|
|
2057
2082
|
* rule" named nothing): a fastened part's place and turn are its fastening's; other numbers are worked out from
|
|
2058
2083
|
* something else. A size that follows something is said at the panel's head, so not again here.
|
|
2059
2084
|
*/
|
|
2085
|
+
/** 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). */
|
|
2086
|
+
private renderSizeKeeps() {
|
|
2087
|
+
const asset = this.asset()
|
|
2088
|
+
const kept = asset && this.selected ? keptBySizeOf(asset, this.selected) : undefined
|
|
2089
|
+
return kept ? html`<p hint size-keeps>${i18next.t(SIZE_KEEPS_WORDS[kept])}</p>` : nothing
|
|
2090
|
+
}
|
|
2060
2091
|
private renderHeldAxes(vectors: FieldGroup[]) {
|
|
2061
2092
|
const fastened = !!(this.asset())?.joints?.some(j => j.body1.part === ownerOf(this.selected))
|
|
2062
2093
|
const held = vectors.filter(g => g.kind === 'vector' && g.name !== 'size' && (['x', 'y', 'z'] as const).some(axis => !g.cells[axis]))
|