@things-factory/figure-ui 10.1.180 → 10.1.183
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/editor-styles.ts +28 -9
- package/client/modeller/editor.ts +105 -12
- package/client/modeller/fasten-form.ts +207 -0
- package/client/modeller/fasten-geometry.ts +5 -1
- package/client/modeller/figure-fasten-dialog.ts +442 -0
- package/client/modeller/figure-fasten-drawing.ts +32 -11
- package/client/modeller/figure-links.ts +56 -300
- package/client/modeller/modeller-keys.ts +4 -4
- package/client/pages/figure-modeller-page.ts +17 -11
- package/dist-client/modeller/editor-styles.js +28 -9
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +23 -2
- package/dist-client/modeller/editor.js +121 -12
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +69 -0
- package/dist-client/modeller/fasten-form.js +131 -0
- package/dist-client/modeller/fasten-form.js.map +1 -0
- package/dist-client/modeller/fasten-geometry.d.ts +3 -1
- package/dist-client/modeller/fasten-geometry.js +6 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +64 -0
- package/dist-client/modeller/figure-fasten-dialog.js +418 -0
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -0
- package/dist-client/modeller/figure-fasten-drawing.d.ts +6 -3
- package/dist-client/modeller/figure-fasten-drawing.js +36 -10
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +15 -39
- package/dist-client/modeller/figure-links.js +58 -301
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +4 -4
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +5 -1
- package/dist-client/pages/figure-modeller-page.js +14 -12
- 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/package.json +14 -14
- package/test/browser/links-page.js +4 -4
- package/test/browser/links.check.mjs +184 -46
- package/test/browser/modeller-page.js +23 -0
- package/test/browser/modeller.check.mjs +19 -20
- package/test/browser/namespace-page.js +2 -29
- package/test/browser/popup-stand-in.js +48 -0
- package/test/browser/product-page.check.mjs +4 -3
- package/test/browser/product-page.js +19 -32
- package/test/browser/props.check.mjs +28 -7
- package/test/browser/screen.check.mjs +28 -23
- package/test/browser/section-page.js +13 -41
- package/test/browser/section.check.mjs +39 -30
- package/test/fasten-form.test.ts +77 -0
- package/test/fasten-geometry.test.ts +2 -0
- package/test/first-minute.test.ts +7 -4
- package/translations/en.json +4 -3
- package/translations/es.json +4 -3
- package/translations/ja.json +4 -3
- package/translations/ko.json +4 -3
- package/translations/ms.json +4 -3
- package/translations/zh.json +4 -3
|
@@ -6,30 +6,24 @@ import './figure-slot-layout-panel.js'
|
|
|
6
6
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
7
7
|
|
|
8
8
|
import { i18next, localize } from '@operato/i18n'
|
|
9
|
-
import {
|
|
9
|
+
import { figureFaceOverhangOf } from '@hatiolab/figure-model'
|
|
10
10
|
import type { FigureAsset, FigureAuthoringAction, FigureDimensionSource, FigureFace } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
acceptsOf,
|
|
14
|
-
alignOf,
|
|
15
14
|
AXES,
|
|
16
15
|
canFasten,
|
|
17
|
-
facesOf,
|
|
18
16
|
canFollow,
|
|
19
17
|
followCommandOf,
|
|
20
18
|
followableAxes,
|
|
21
19
|
positionIsShare,
|
|
22
20
|
choiceOf,
|
|
23
21
|
followWordsOf,
|
|
24
|
-
fastenedTo,
|
|
25
22
|
heldBy,
|
|
26
23
|
followOf,
|
|
27
|
-
hangsFrom,
|
|
28
24
|
shownWhenOf,
|
|
29
25
|
visibilityControlsOf,
|
|
30
|
-
placeKeptOf,
|
|
31
26
|
fasteningOf,
|
|
32
|
-
fastenGapOf,
|
|
33
27
|
hasInstanceSize,
|
|
34
28
|
holdsThings,
|
|
35
29
|
leadersFor,
|
|
@@ -46,42 +40,20 @@ import {
|
|
|
46
40
|
fixedJointOf,
|
|
47
41
|
rowOf,
|
|
48
42
|
positionsWorkedOut,
|
|
49
|
-
fieldWordsOf,
|
|
50
43
|
measureNow,
|
|
51
|
-
ownFacesOf,
|
|
52
|
-
alignRowsOf,
|
|
53
|
-
type Alignment,
|
|
54
44
|
type Axis,
|
|
55
45
|
type Follow,
|
|
56
46
|
type MotionForm
|
|
57
47
|
} from './links.js'
|
|
58
48
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
59
|
-
import './
|
|
60
|
-
import
|
|
61
|
-
import { faceViewOf, insideAlignmentOf, modeOf, myFaceFor, turnsInPlace, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
49
|
+
import { insideAlignmentOf } from './fasten-geometry.js'
|
|
50
|
+
import { fastenFormOf, fastenPlanOf, fastenSentenceOf } from './fasten-form.js'
|
|
62
51
|
import type { FigureRevision } from './revision.js'
|
|
63
52
|
import { HelpToggles } from './help.js'
|
|
64
53
|
import { groupMenu, GroupMenuStyles } from './group-menu.js'
|
|
65
54
|
import { jointAxisWord } from './joint-axis.js'
|
|
66
55
|
import { placeTerms } from '@things-factory/figure-viewer/dist-client/detail-facts.js'
|
|
67
56
|
|
|
68
|
-
const FACES = Object.keys(FIGURE_FACES) as FigureFace[]
|
|
69
|
-
/** The target list's entry for the plane the figure stands by. */
|
|
70
|
-
const PLANE = '__plane'
|
|
71
|
-
|
|
72
|
-
const freshFasten = () => ({
|
|
73
|
-
face: 'bottom' as FigureFace | 'centre',
|
|
74
|
-
toPlane: false,
|
|
75
|
-
target: '',
|
|
76
|
-
targetFace: 'top' as FigureFace,
|
|
77
|
-
facing: 'meet' as 'meet' | 'flush',
|
|
78
|
-
gap: 0,
|
|
79
|
-
gapKept: undefined as unknown,
|
|
80
|
-
twist: 0,
|
|
81
|
-
align: {} as Partial<Record<Axis, Alignment>>,
|
|
82
|
-
alignMm: {} as Partial<Record<Axis, number>>,
|
|
83
|
-
alignInput: {} as Partial<Record<Axis, string>>
|
|
84
|
-
})
|
|
85
57
|
const freshMotion = (): MotionForm => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: '', travelPlus: 0 })
|
|
86
58
|
const freshRow = () => ({ axis: '' as Axis | '', pitch: 200, fills: true, count: 5 })
|
|
87
59
|
const freshPlace = () => ({ role: 'grip' as 'seat' | 'grip', face: 'top' as FigureFace, label: '', accepts: '', capacity: 1 })
|
|
@@ -96,7 +68,8 @@ const freshPlace = () => ({ role: 'grip' as 'seat' | 'grip', face: 'top' as Figu
|
|
|
96
68
|
*
|
|
97
69
|
* The sections are dimensions, placement and joint, in that order. Every
|
|
98
70
|
* change is one figure-model authoring command, sent as `command` and run by the editor through the session — one
|
|
99
|
-
* step of the undo history.
|
|
71
|
+
* step of the undo history. The fastening is said here in one line; it is chosen in the fastening window
|
|
72
|
+
* (`figure-fasten-dialog.ts`), which the editor opens when this asks (`fasten-open`). Choices the command would refuse (a round body fastened, a size following an instance
|
|
100
73
|
* size the figure does not declare) are not offered, so the author is not sent into a refusal.
|
|
101
74
|
*/
|
|
102
75
|
@customElement('figure-links')
|
|
@@ -148,7 +121,6 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
148
121
|
details[resize-choice] { margin: 0; padding: 0; border: 0; }
|
|
149
122
|
details[resize-choice] > div { display: grid; gap: 8px; padding: 8px 0; }
|
|
150
123
|
p[fasten-mode] { margin: 0; font-weight: 600; }
|
|
151
|
-
section[fastening][spotlit] { outline: 2px solid var(--md-sys-color-primary, #477b9e); outline-offset: 4px; border-radius: 6px; transition: outline-color 0.3s; }
|
|
152
124
|
details { margin-top: 8px; }
|
|
153
125
|
details[row-fold] { margin: 0; }
|
|
154
126
|
details[row-fold] > summary { padding: 0; color: var(--md-sys-color-on-surface); }
|
|
@@ -162,7 +134,6 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
162
134
|
div[overhang] { display: grid; gap: 6px; margin: 8px 0; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--md-sys-color-outline-variant, #8884); }
|
|
163
135
|
div[overhang] p { margin: 0; }
|
|
164
136
|
ul[detach-words] { margin: 4px 0 10px; padding-left: 20px; display: grid; gap: 2px; }
|
|
165
|
-
span[fasten-summary] { opacity: 0.8; }
|
|
166
137
|
/* The difference a following measure keeps: under its choice, on the same line as its words. */
|
|
167
138
|
label[inline-plus] {
|
|
168
139
|
display: flex;
|
|
@@ -186,11 +157,12 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
186
157
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
187
158
|
@property() section: 'shape' | 'response' | 'places' | 'motion' = 'shape'
|
|
188
159
|
/**
|
|
189
|
-
* Its tab is the one shown
|
|
190
|
-
*
|
|
160
|
+
* Its tab is the one shown, and no window over it speaks instead. The response's row speaks of what a command would
|
|
161
|
+
* draw, and the properties' says there is none: only the one in sight may, as the hidden one's silence would clear what
|
|
162
|
+
* the shown one draws. While the fastening window is open it alone speaks (the editor sets this false).
|
|
191
163
|
*/
|
|
192
164
|
@property({ type: Boolean }) active = true
|
|
193
|
-
/** What the
|
|
165
|
+
/** What the row's command would do, in words (the editor works it out: `command-preview.ts`). */
|
|
194
166
|
@property({ attribute: false }) previewWords: string[] = []
|
|
195
167
|
/** Where the selected part's motion takes it, at both ends of its range (the editor draws them on the canvas). */
|
|
196
168
|
@property({ attribute: false }) motionEnds: { control: string; min: number; max: number; unit: string } | null = null
|
|
@@ -201,23 +173,15 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
201
173
|
/** What taking this part off would do, said beside the buttons that do it (worked out on a copy by the editor). */
|
|
202
174
|
@property({ attribute: false }) detachWords: string[] = []
|
|
203
175
|
/*
|
|
204
|
-
|
|
205
|
-
|
|
176
|
+
What a section speaks of on the canvas: the response's row while its fold is open; the properties', the fastening the
|
|
177
|
+
part has -- its face lit, as the one-line result says it.
|
|
206
178
|
*/
|
|
207
179
|
private candidate?: { action: FigureAuthoringAction; face?: { part: string; face: string } }
|
|
208
|
-
private told = ''
|
|
209
|
-
/* Two forms describe a command here: the fastening and the row. The one being worked on is the one seen. */
|
|
210
|
-
@state() private working: 'fasten' | 'row' = 'fasten'
|
|
211
180
|
private fastenCandidate?: { action: FigureAuthoringAction; face?: { part: string; face: string } }
|
|
181
|
+
private told = ''
|
|
182
|
+
@state() private rowOpen = false
|
|
212
183
|
private rowCandidate?: FigureAuthoringAction
|
|
213
184
|
|
|
214
|
-
/** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
|
|
215
|
-
@state() private choosingFastening = false
|
|
216
|
-
/** The fastening the part has, opened to be changed: until then the place group says it in one line. */
|
|
217
|
-
@state() private changingFastening = false
|
|
218
|
-
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
219
|
-
@state() private spotlit = false
|
|
220
|
-
@state() private fasten = freshFasten()
|
|
221
185
|
/** Why 정렬·간격 고치기 moved nothing, said under it; empty once it moved the part or the part changed. */
|
|
222
186
|
@state() private fixNote = ''
|
|
223
187
|
@state() private motion: MotionForm = freshMotion()
|
|
@@ -242,15 +206,10 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
242
206
|
if (changed.has('asset') || changed.has('partId')) this.fixNote = ''
|
|
243
207
|
if (this.asset && this.partId && this.partId !== this.loadedFor) {
|
|
244
208
|
this.loadedFor = this.partId
|
|
245
|
-
/* A part's own fastening is not another part's choices: the form opens on it (walk 1, 2026-09-26). */
|
|
246
|
-
const own = this.revision && this.partId ? fasteningOf(this.revision, this.partId) : null
|
|
247
|
-
this.fasten = own ? ({ ...freshFasten(), ...own } as ReturnType<typeof freshFasten>) : freshFasten()
|
|
248
209
|
this.motion = freshMotion()
|
|
249
210
|
this.place = freshPlace()
|
|
250
211
|
this.row = freshRow()
|
|
251
212
|
this.editingMotion = false
|
|
252
|
-
this.choosingFastening = false
|
|
253
|
-
this.changingFastening = false
|
|
254
213
|
}
|
|
255
214
|
}
|
|
256
215
|
|
|
@@ -329,7 +288,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
329
288
|
: nothing}
|
|
330
289
|
${rowOf(asset, this.partId) ? html`<slot name="row"></slot>` : this.renderRow(asset)}
|
|
331
290
|
`
|
|
332
|
-
this.candidate = this.
|
|
291
|
+
this.candidate = this.rowOpen && this.rowCandidate ? { action: this.rowCandidate } : undefined
|
|
333
292
|
return response
|
|
334
293
|
}
|
|
335
294
|
const shown = html`
|
|
@@ -337,7 +296,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
337
296
|
<h3>${i18next.t('figure.label.group-size')}</h3>
|
|
338
297
|
<slot name="size"></slot>
|
|
339
298
|
</section>
|
|
340
|
-
<section fastening
|
|
299
|
+
<section fastening>
|
|
341
300
|
<h3>
|
|
342
301
|
${i18next.t('figure.label.group-place')}${this.help.button('figure.text.fastening-explained')}
|
|
343
302
|
${groupMenu([{ label: i18next.t('figure.button.detach'), icon: 'link_off', disabled: this.locked || !held, run: () => this.command({ kind: 'detach', part: this.partId }) }])}
|
|
@@ -494,53 +453,36 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
494
453
|
})}</div></details>`
|
|
495
454
|
}
|
|
496
455
|
|
|
497
|
-
/**
|
|
498
|
-
* "Open attachment settings": the fastening this part has now -- its face, what it is on, the gap -- in the form below,
|
|
499
|
-
* brought into view with the focus on it. The form is loaded again first: the part was fastened by "follow size,
|
|
500
|
-
* attached" while it was selected, so the form still held what it had before (designer 2026-09-27: it opened the
|
|
501
|
-
* holding places instead).
|
|
502
|
-
*/
|
|
503
|
-
/** The face the part sits on, in view with the focus on it: where the place across the face is dragged or keyed. */
|
|
504
456
|
/**
|
|
505
457
|
* 정렬·간격 고치기: the part moved inside the face it is fastened on, as one step -- the least move on each side it is
|
|
506
458
|
* out, or centred when it is wider than the face. When nothing of it can be moved, why is said under the button, not
|
|
507
|
-
* nothing at all (:3300, 2026-10-11: the button only scrolled to the drawing, which was already in view).
|
|
459
|
+
* nothing at all (:3300, 2026-10-11: the button only scrolled to the drawing, which was already in view). The command
|
|
460
|
+
* is the fastening window's for the same choice (`fasten-form.ts`), with only the place across the face changed.
|
|
508
461
|
*/
|
|
509
|
-
private
|
|
462
|
+
private fixAlignment() {
|
|
463
|
+
const revision = this.revision
|
|
464
|
+
if (!revision) return
|
|
465
|
+
const form = fastenFormOf(revision, this.partId)
|
|
510
466
|
/* Whether it is out is the kernel's measure (a round face is not measured there); the drawing's sizes say how far to move. */
|
|
511
|
-
const own =
|
|
467
|
+
const own = figureFaceOverhangOf(revision.asset, this.partId, revision.inputs)
|
|
512
468
|
const fixed =
|
|
513
469
|
own?.state === 'unknown'
|
|
514
470
|
? ({ cannot: 'unmeasured' } as const)
|
|
515
471
|
: own?.state === 'out' || own?.state === 'possible'
|
|
516
|
-
? insideAlignmentOf(
|
|
472
|
+
? insideAlignmentOf(fastenPlanOf(revision, this.partId, form).view, form.align, form.alignMm)
|
|
517
473
|
: ({ cannot: 'inside' } as const)
|
|
518
474
|
if ('cannot' in fixed) {
|
|
519
|
-
this.fixNote = i18next.t(`figure.text.fix-alignment-${fixed.cannot}`, { part: this.partId, on: target })
|
|
475
|
+
this.fixNote = i18next.t(`figure.text.fix-alignment-${fixed.cannot}`, { part: this.partId, on: form.target })
|
|
520
476
|
return
|
|
521
477
|
}
|
|
522
478
|
this.fixNote = ''
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/** The fastening form, open on this part and in view: for "fasten it first" from elsewhere. */
|
|
528
|
-
async showFastening() {
|
|
529
|
-
if (this.asset) await this.openFastening(this.asset)
|
|
479
|
+
const action = fastenPlanOf(revision, this.partId, { ...form, align: fixed.align, alignMm: fixed.alignMm }).action
|
|
480
|
+
if (action) this.command(action)
|
|
530
481
|
}
|
|
531
482
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
this.
|
|
535
|
-
/* Asked for from elsewhere: the form itself, not the one-line summary of the fastening it has. */
|
|
536
|
-
this.changingFastening = true
|
|
537
|
-
this.spotlit = true
|
|
538
|
-
await this.updateComplete
|
|
539
|
-
const section = this.renderRoot.querySelector<HTMLElement>('section[fastening]')
|
|
540
|
-
section?.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
|
541
|
-
/* Into the form itself, not the section's help button. */
|
|
542
|
-
;(section?.querySelector<HTMLElement>('select, input') ?? section?.querySelector<HTMLElement>('button'))?.focus({ preventScroll: true })
|
|
543
|
-
setTimeout(() => (this.spotlit = false), 1600)
|
|
483
|
+
/** The fastening window, asked of the editor: it opens over the canvas on what the part has now. */
|
|
484
|
+
private openFastening() {
|
|
485
|
+
this.dispatchEvent(new CustomEvent('fasten-open', { bubbles: true, composed: true }))
|
|
544
486
|
}
|
|
545
487
|
|
|
546
488
|
/**
|
|
@@ -597,247 +539,61 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
597
539
|
}
|
|
598
540
|
|
|
599
541
|
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
542
|
+
* The fastening in one line -- what, on which face, how -- and 「바꾸기」 to open the window where it is chosen (chief
|
|
543
|
+
* architect 2026-10-16; user 2026-10-17: the result here, the editing in a window with room). Taking it off is in the
|
|
544
|
+
* group's 「⋯」. What hangs over the face, and the way to fix it, stay here: they are about the fastening as it is now.
|
|
602
545
|
*/
|
|
603
|
-
private aimAt(asset: FigureAsset, f: ReturnType<typeof freshFasten>) {
|
|
604
|
-
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
605
|
-
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]!
|
|
606
|
-
const aim = f.toPlane ? ('plane' as const) : f.target ? { part: f.target, face: targetFace } : undefined
|
|
607
|
-
this.fasten = { ...f, ...(aim ? placeKeptOf(this.revision!, this.partId, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) }
|
|
608
|
-
}
|
|
609
|
-
|
|
610
546
|
private renderFastening(asset: FigureAsset) {
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
547
|
+
const revision = this.revision!
|
|
548
|
+
const own = fasteningOf(revision, this.partId)
|
|
549
|
+
/* Placed by an origin (ADR-0107 decision 4): attach writes faces, so the window would move it off its origin. Said, and taken off only. */
|
|
550
|
+
if (own?.atOrigin)
|
|
615
551
|
return html`<div fastened fasten-at-origin>
|
|
616
552
|
<p>${this.heldWords(asset)}</p>
|
|
617
553
|
${this.renderDetachWords()}
|
|
618
554
|
</div>`
|
|
619
|
-
}
|
|
620
|
-
const f = this.fasten
|
|
621
|
-
/* Not a part that hangs from this one: fastening a frame to its own roller would close a loop. */
|
|
622
|
-
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one))
|
|
623
|
-
/* Shown, not dropped, and not choosable, with the reason (ADR-0097 §5.5): fastening it would close a loop. */
|
|
624
|
-
const loops = (one: string) => hangsFrom(asset, one, this.partId)
|
|
625
|
-
const planeWord = i18next.t('figure.label.resize-place-' + (asset.placement ?? 'floor'))
|
|
626
|
-
/* The faces are the ones each shape declares: a part is fastened by one of its outer faces, to any face of the target. */
|
|
627
|
-
const mineFaces = ownFacesOf(asset, this.partId, f.toPlane)
|
|
628
|
-
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
629
|
-
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]!
|
|
630
|
-
const aim = f.toPlane ? ('plane' as const) : { part: f.target, face: targetFace }
|
|
631
|
-
/* The part's own face is not chosen: the target's face and how give it (user 2026-10-01; the kernel takes no other). */
|
|
632
|
-
const mode: FastenMode = f.toPlane ? 'meet' : modeOf(f.face, f.facing)
|
|
633
|
-
const derived = f.toPlane || f.target ? myFaceFor(this.revision!, this.partId, aim, mode) : undefined
|
|
634
|
-
const myFace = derived ?? (mineFaces.includes(f.face) ? f.face : mineFaces[0]!)
|
|
635
|
-
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist, f.alignInput) : undefined
|
|
636
|
-
const action =
|
|
637
|
-
f.toPlane || f.target
|
|
638
|
-
? ({
|
|
639
|
-
kind: 'attach',
|
|
640
|
-
part: this.partId,
|
|
641
|
-
face: myFace,
|
|
642
|
-
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
643
|
-
...(f.toPlane ? {} : { facing: f.facing }),
|
|
644
|
-
...fastenGapOf(f),
|
|
645
|
-
twist: f.twist,
|
|
646
|
-
...(align ? { align } : {}),
|
|
647
|
-
replace: !!held
|
|
648
|
-
} as FigureAuthoringAction)
|
|
649
|
-
: undefined
|
|
650
|
-
if (action) this.fastenCandidate = { action, ...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } }) }
|
|
651
|
-
/*
|
|
652
|
-
What the folded details hold that is not their default, said while folded; the way and the place on the face are
|
|
653
|
-
the drawing's, and said there (user 2026-10-02: the details had selects repeating the drawing). Nothing is said
|
|
654
|
-
for the default.
|
|
655
|
-
*/
|
|
656
|
-
const detail = [
|
|
657
|
-
...(f.gapKept !== undefined
|
|
658
|
-
? [i18next.t('figure.text.fasten-summary-gap-worked-out')]
|
|
659
|
-
: f.gap
|
|
660
|
-
? [i18next.t('figure.text.fasten-summary-gap', { mm: String(f.gap) })]
|
|
661
|
-
: []),
|
|
662
|
-
...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
|
|
663
|
-
]
|
|
664
|
-
/* Across the face, a row only where the drawing cannot set it: no drawing of the face, or a share or a field to follow. */
|
|
665
|
-
const view = f.toPlane || f.target ? faceViewOf(this.revision!, this.partId, aim) : undefined
|
|
666
|
-
const alignRows = f.toPlane || f.target ? alignRowsOf(asset, this.partId, aim, view, !!f.twist, f.alignInput) : []
|
|
667
|
-
/* A fastening the part has is changed as soon as something is chosen, as the drawing does; a new one waits for 붙이기. */
|
|
668
|
-
const settle = () => this.onFastenRelease(!!held)
|
|
669
|
-
/*
|
|
670
|
-
Fastening it anew is said, so the form is not taken for the one it has (designer 2026-09-27). The one it has is
|
|
671
|
-
said once, by the sentence over the drawing -- the panel's head says it too; a third and a fourth line saying the
|
|
672
|
-
same (「현재 관계 수정」, 「붙은 곳」) went (user 2026-10-01, the panel checked for balance).
|
|
673
|
-
*/
|
|
674
|
-
const current = asset.joints?.find(j => j.body1.part === this.partId)
|
|
675
|
-
const editing = i18next.t('figure.text.fasten-new')
|
|
676
|
-
/*
|
|
677
|
-
A fastening the part has, said in one line -- what, on which face, how -- and 「바꾸기」 to open the drawing and the
|
|
678
|
-
details (chief architect 2026-10-16). They were open on every fastened part, near a screen of the panel, for a
|
|
679
|
-
choice made once. Taking it off is in the group's 「⋯」. What would hang over still shows: it is about now.
|
|
680
|
-
*/
|
|
681
|
-
/* What hangs over the face, and the way to fix it: about the fastening as it is now, so said open or folded. */
|
|
682
555
|
const overhang = html`${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
683
556
|
${this.overhangWords.length
|
|
684
557
|
? html`<div overhang role="status">
|
|
685
558
|
${this.overhangWords.map(line => html`<p>${line}</p>`)}
|
|
686
|
-
${
|
|
687
|
-
? html`<button act fix-alignment ?disabled=${this.locked} @click=${() => this.fixAlignment(
|
|
559
|
+
${own
|
|
560
|
+
? html`<button act fix-alignment ?disabled=${this.locked} @click=${() => this.fixAlignment()}>
|
|
688
561
|
<md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}
|
|
689
562
|
</button>
|
|
690
563
|
${this.fixNote ? html`<p hint fix-note>${this.fixNote}</p>` : nothing}`
|
|
691
564
|
: nothing}
|
|
692
565
|
</div>`
|
|
693
566
|
: nothing}`
|
|
694
|
-
if (
|
|
567
|
+
if (own && heldBy(asset, this.partId)) {
|
|
568
|
+
const form = fastenFormOf(revision, this.partId)
|
|
569
|
+
const plan = fastenPlanOf(revision, this.partId, form)
|
|
570
|
+
if (plan.action) this.fastenCandidate = { action: plan.action, ...(plan.face ? { face: plan.face } : {}) }
|
|
571
|
+
const planeWord = i18next.t('figure.label.resize-place-' + (asset.placement ?? 'floor'))
|
|
695
572
|
return html`
|
|
696
573
|
${overhang}
|
|
697
574
|
<div fastened-summary>
|
|
698
|
-
<p fasten-sentence>${this.
|
|
699
|
-
<button act change-fastening ?disabled=${this.locked} @click=${() =>
|
|
575
|
+
<p fasten-sentence>${fastenSentenceOf(this.partId, form, plan, planeWord, (key, params) => i18next.t(key, params))}</p>
|
|
576
|
+
<button act change-fastening ?disabled=${this.locked} @click=${() => this.openFastening()}>${i18next.t('figure.button.change-fastening')}</button>
|
|
700
577
|
</div>
|
|
701
578
|
${this.renderDetachWords()}
|
|
702
579
|
`
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
return html`
|
|
706
|
-
<p hint>${i18next.t('figure.text.fasten-start')}</p>
|
|
707
|
-
<button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
|
|
708
|
-
<md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
|
|
709
|
-
</button>
|
|
710
|
-
`
|
|
580
|
+
}
|
|
581
|
+
/* A blank attachment is not a form to decipher: what it does, and the way to the window. */
|
|
711
582
|
return html`
|
|
712
|
-
${current ? nothing : html`<p fasten-mode>${editing}</p>`}
|
|
713
583
|
${overhang}
|
|
714
|
-
|
|
715
|
-
<
|
|
716
|
-
<
|
|
717
|
-
|
|
718
|
-
const value = (e.target as HTMLSelectElement).value
|
|
719
|
-
this.aimAt(asset, value === PLANE ? { ...f, toPlane: true, target: '' } : { ...f, toPlane: false, target: value })
|
|
720
|
-
if (value) settle()
|
|
721
|
-
}}>
|
|
722
|
-
<option value="" ?selected=${!f.target && !f.toPlane}>${i18next.t('figure.label.target-part-placeholder')}</option>
|
|
723
|
-
<option value=${PLANE} ?selected=${f.toPlane}>${planeWord}</option>
|
|
724
|
-
${targets.map(one =>
|
|
725
|
-
loops(one)
|
|
726
|
-
? html`<option value=${one} disabled>${i18next.t('figure.text.target-would-loop', { part: one, mine: this.partId })}</option>`
|
|
727
|
-
: html`<option value=${one} ?selected=${one === f.target && !f.toPlane}>${one}</option>`
|
|
728
|
-
)}
|
|
729
|
-
</select>
|
|
730
|
-
</div>
|
|
731
|
-
${f.toPlane || f.target
|
|
732
|
-
? html`<p fasten-sentence>${this.sentenceOf(myFace, f.toPlane ? planeWord : `${f.target}의 ${i18next.t('figure.label.face-' + targetFace)}`, mode)}</p>
|
|
733
|
-
<figure-fasten-drawing
|
|
734
|
-
.target=${f.toPlane ? '' : f.target}
|
|
735
|
-
.planeWord=${planeWord}
|
|
736
|
-
.faces=${theirFaces}
|
|
737
|
-
.face=${targetFace}
|
|
738
|
-
.modes=${f.toPlane ? (['meet'] as FastenMode[]) : ([...(['meet', 'flush'] as FastenMode[]), ...(turnsInPlace(asset, this.partId) ? (['centre'] as FastenMode[]) : [])])}
|
|
739
|
-
.mode=${mode}
|
|
740
|
-
.view=${view}
|
|
741
|
-
.align=${f.align}
|
|
742
|
-
.alignMm=${f.alignMm}
|
|
743
|
-
.gap=${f.gap}
|
|
744
|
-
.partName=${this.partId}
|
|
745
|
-
?locked=${this.locked}
|
|
746
|
-
?said=${!!held}
|
|
747
|
-
@fasten-change=${(e: CustomEvent<FastenChange>) => this.onFastenChange(asset, e.detail)}
|
|
748
|
-
@fasten-release=${() => this.onFastenRelease(!!held)}
|
|
749
|
-
></figure-fasten-drawing>`
|
|
750
|
-
: html`<p hint>${i18next.t('figure.text.fasten-target-first')}</p>`}
|
|
751
|
-
${(f.toPlane || f.target) ? html`<details fasten-detail>
|
|
752
|
-
<summary>${i18next.t('figure.label.fasten-detail')}${detail.length ? html` · <span fasten-summary>${detail.join(' · ')}</span>` : nothing}</summary>
|
|
753
|
-
${alignRows.length ? this.renderAlignment(alignRows, settle) : nothing}
|
|
754
|
-
<div form>
|
|
755
|
-
<span>${i18next.t('figure.label.gap')}</span>
|
|
756
|
-
<input
|
|
757
|
-
type="number"
|
|
758
|
-
step="any"
|
|
759
|
-
.value=${f.gapKept !== undefined ? '' : String(f.gap)}
|
|
760
|
-
placeholder=${f.gapKept !== undefined ? i18next.t('figure.label.follows-expression') : ''}
|
|
761
|
-
@change=${(e: Event) => ((this.fasten = { ...f, gap: Number((e.target as HTMLInputElement).value) || 0, gapKept: undefined }), settle())}
|
|
762
|
-
/>
|
|
763
|
-
<span>${i18next.t('figure.label.twist')}</span>
|
|
764
|
-
<input type="number" step="any" .value=${String(f.twist)} @change=${(e: Event) => ((this.fasten = { ...f, twist: Number((e.target as HTMLInputElement).value) || 0 }), settle())} />
|
|
765
|
-
</div>
|
|
766
|
-
</details>
|
|
767
|
-
${/* What 붙이기 would do; a fastening the part has is changed at once, and the canvas shows it. */ action && !held && this.working === 'fasten' ? this.renderPreviewWords() : nothing}
|
|
768
|
-
${held
|
|
769
|
-
? nothing
|
|
770
|
-
: html`<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
771
|
-
<md-icon>link</md-icon>${i18next.t('figure.button.fasten')}
|
|
772
|
-
</button>`}` : nothing}
|
|
584
|
+
<p hint>${i18next.t('figure.text.fasten-start')}</p>
|
|
585
|
+
<button act fasten-start ?disabled=${this.locked} @click=${() => this.openFastening()}>
|
|
586
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
|
|
587
|
+
</button>
|
|
773
588
|
`
|
|
774
589
|
}
|
|
775
590
|
|
|
776
|
-
/** 「lid 의 아랫면을 bin 의 윗면에 맞닿게」: what the fastening is, in one line, the part's own face said as a result. */
|
|
777
|
-
private sentenceOf(myFace: string, to: string, mode: FastenMode) {
|
|
778
|
-
return i18next.t(`figure.text.fasten-sentence-${mode}`, { part: this.partId, face: i18next.t('figure.label.face-' + myFace), to })
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/** A change the drawing asks: to the form only; the preview behind follows the form. */
|
|
782
|
-
private onFastenChange(asset: FigureAsset, c: FastenChange) {
|
|
783
|
-
const f = this.fasten
|
|
784
|
-
if ('targetFace' in c) this.aimAt(asset, { ...f, targetFace: c.targetFace as FigureFace })
|
|
785
|
-
else if ('mode' in c) this.fasten = { ...f, face: c.mode === 'centre' ? 'centre' : f.face === 'centre' ? 'bottom' : f.face, facing: c.mode === 'flush' ? 'flush' : 'meet' }
|
|
786
|
-
else if ('gap' in c) this.fasten = { ...f, gap: c.gap, gapKept: undefined }
|
|
787
|
-
else
|
|
788
|
-
this.fasten = {
|
|
789
|
-
...f,
|
|
790
|
-
align: { ...f.align, [c.axis]: c.how },
|
|
791
|
-
alignMm: c.how === 'mm' ? { ...f.alignMm, [c.axis]: c.mm ?? 0 } : f.alignMm
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/** Let go: a fastening the part already has is applied as it now is (one step); a new one waits for 붙이기. */
|
|
796
|
-
private async onFastenRelease(held: boolean) {
|
|
797
|
-
if (!held) return
|
|
798
|
-
await this.updateComplete
|
|
799
|
-
if (this.fastenCandidate) this.command(this.fastenCandidate.action)
|
|
800
|
-
}
|
|
801
|
-
|
|
802
591
|
private renderDetachWords() {
|
|
803
592
|
/* One thing to say is a line, not a list of one. */
|
|
804
593
|
if (this.detachWords.length === 1) return html`<p hint detach-words>${this.detachWords[0]}</p>`
|
|
805
594
|
return this.detachWords.length ? html`<ul detach-words>${this.detachWords.map(line => html`<li>${line}</li>`)}</ul>` : nothing
|
|
806
595
|
}
|
|
807
596
|
|
|
808
|
-
/**
|
|
809
|
-
* Where on the target face the part sits, on the two axes across it: centred, level with the target's edge on
|
|
810
|
-
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
811
|
-
*/
|
|
812
|
-
private renderAlignment(axes: { axis: Axis; edges: boolean }[], settle: () => void) {
|
|
813
|
-
const f = this.fasten
|
|
814
|
-
const set = (axis: Axis, how: Alignment) => ((this.fasten = { ...f, align: { ...f.align, [axis]: how } }), settle())
|
|
815
|
-
const setMm = (axis: Axis, mm: number) => ((this.fasten = { ...f, alignMm: { ...f.alignMm, [axis]: mm } }), settle())
|
|
816
|
-
return html`<div form>
|
|
817
|
-
${axes.map(({ axis, edges }) => {
|
|
818
|
-
const how = f.align[axis] === 'min' || f.align[axis] === 'max' ? (edges ? f.align[axis]! : 'centre') : (f.align[axis] ?? 'centre')
|
|
819
|
-
return html`<span>${i18next.t('figure.label.align-' + axis)}</span>
|
|
820
|
-
<select @change=${(e: Event) => set(axis, (e.target as HTMLSelectElement).value as Alignment)}>
|
|
821
|
-
<option value="centre" ?selected=${how === 'centre'}>${i18next.t('figure.label.align-centre')}</option>
|
|
822
|
-
${edges
|
|
823
|
-
? html`<option value="min" ?selected=${how === 'min'}>${i18next.t(`figure.label.align-${axis}-min`)}</option>
|
|
824
|
-
<option value="max" ?selected=${how === 'max'}>${i18next.t(`figure.label.align-${axis}-max`)}</option>`
|
|
825
|
-
: nothing}
|
|
826
|
-
${f.alignInput?.[axis]
|
|
827
|
-
? html`<option value="input" ?selected=${how === 'input'}>${i18next.t('figure.label.align-input', { field: fieldWordsOf(f.alignInput[axis]!, key => i18next.t(key)) })}</option>`
|
|
828
|
-
: nothing}
|
|
829
|
-
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
830
|
-
${this.asset && hasInstanceSize(this.asset, axis)
|
|
831
|
-
? html`<option value="share" ?selected=${how === 'share'}>${i18next.t('figure.label.align-share')}</option>`
|
|
832
|
-
: nothing}
|
|
833
|
-
</select>
|
|
834
|
-
${how === 'mm' || how === 'share'
|
|
835
|
-
? html`<span></span><input type="number" step="any" aria-label=${i18next.t('figure.label.align-mm')} .value=${String(f.alignMm[axis] ?? 0)} @change=${(e: Event) => setMm(axis, Number((e.target as HTMLInputElement).value) || 0)} />`
|
|
836
|
-
: nothing}`
|
|
837
|
-
})}
|
|
838
|
-
</div>`
|
|
839
|
-
}
|
|
840
|
-
|
|
841
597
|
/**
|
|
842
598
|
* Making the part a row across what it is fastened to (figure-model `make-repeat`): along one of the axes across its
|
|
843
599
|
* face, copies a pitch apart filling the parent's length, or a count spread over it. Once made, the row's own section
|
|
@@ -868,7 +624,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
868
624
|
${head}
|
|
869
625
|
<p hint row-blocked>${i18next.t(`figure.text.row-blocked-${blocked ?? 'unfastened'}`, { part: this.partId })}</p>
|
|
870
626
|
${blocked === 'unfastened'
|
|
871
|
-
? html`<button act @click=${() => this.
|
|
627
|
+
? html`<button act @click=${() => this.dispatchEvent(new CustomEvent('go-fasten', { bubbles: true, composed: true }))}><md-icon>link</md-icon>${i18next.t('figure.button.go-fasten')}</button>`
|
|
872
628
|
: nothing}
|
|
873
629
|
</details></section>`
|
|
874
630
|
const axis = axes.includes(r.axis as Axis) ? (r.axis as Axis) : axes[0]!
|
|
@@ -877,13 +633,13 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
877
633
|
? ({ kind: 'make-repeat', part: this.partId, axis, pitch: r.fills ? r.pitch : Math.max(r.pitch, 1), ...(r.fills ? {} : { count: r.count }) } as FigureAuthoringAction)
|
|
878
634
|
: undefined
|
|
879
635
|
this.rowCandidate = action
|
|
880
|
-
const set = (next: typeof r) => ((this.row = next), (this.
|
|
636
|
+
const set = (next: typeof r) => ((this.row = next), (this.rowOpen = true))
|
|
881
637
|
/*
|
|
882
638
|
Whether the row is being worked follows the fold's own toggle. A press on the heading focuses it before the click;
|
|
883
639
|
set on focus, the fold opened there and the click then folded it again (2026-10-11: it took two clicks to open).
|
|
884
640
|
*/
|
|
885
|
-
const toggled = (e: Event) => (this.
|
|
886
|
-
return html`<section row><details row-fold ?open=${this.
|
|
641
|
+
const toggled = (e: Event) => (this.rowOpen = (e.target as HTMLDetailsElement).open)
|
|
642
|
+
return html`<section row><details row-fold ?open=${this.rowOpen} @toggle=${toggled}>
|
|
887
643
|
${head}
|
|
888
644
|
<div form>
|
|
889
645
|
<span>${i18next.t('figure.label.repeat-axis')}</span>
|
|
@@ -905,7 +661,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
905
661
|
<input type="number" step="1" min="1" .value=${String(r.count)} @change=${(e: Event) => set({ ...r, count: Number((e.target as HTMLInputElement).value) })} />
|
|
906
662
|
</div>
|
|
907
663
|
<p hint>${i18next.t('figure.text.row-count-spreads')}</p>`}
|
|
908
|
-
${action && this.
|
|
664
|
+
${action && this.rowOpen ? this.renderPreviewWords() : nothing}
|
|
909
665
|
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
910
666
|
<md-icon>view_week</md-icon>${i18next.t('figure.button.make-repeat')}
|
|
911
667
|
</button>
|
|
@@ -36,11 +36,11 @@ export class ModellerKeys implements ReactiveController {
|
|
|
36
36
|
const canvas = this.of.scene()
|
|
37
37
|
const path = e.composedPath()
|
|
38
38
|
/*
|
|
39
|
-
A key for something open over the work -- a group's 「⋯」 menu, the view or the history, a question -- is
|
|
40
|
-
thing's: heard here first, Esc deselected the part and was kept from the menu, which then could not be closed
|
|
41
|
-
the keyboard (2026-10-16).
|
|
39
|
+
A key for something open over the work -- a group's 「⋯」 menu, the view or the history, a question, a popup -- is
|
|
40
|
+
that thing's: heard here first, Esc deselected the part and was kept from the menu, which then could not be closed
|
|
41
|
+
by the keyboard (2026-10-16).
|
|
42
42
|
*/
|
|
43
|
-
if (path.some(node => node instanceof Element && (node.localName.startsWith('ox-popup') || node.getAttribute('role') === 'dialog'))) return
|
|
43
|
+
if (path.some(node => node instanceof Element && (node.localName.startsWith('ox-popup') || node.localName === 'ox-floating-overlay' || node.getAttribute('role') === 'dialog'))) return
|
|
44
44
|
const action = keyActionOf(
|
|
45
45
|
{
|
|
46
46
|
key: e.key,
|
|
@@ -413,25 +413,31 @@ export class FigureModellerPage extends PageView {
|
|
|
413
413
|
window.removeEventListener('beforeunload', this.holdUnsaved)
|
|
414
414
|
super.disconnectedCallback()
|
|
415
415
|
}
|
|
416
|
-
|
|
416
|
+
get addressDirty(): boolean { return this.session.read().dirty }
|
|
417
|
+
|
|
418
|
+
get deactivation(): boolean | Parameters<typeof OxPrompt.open>[0] {
|
|
417
419
|
const view = this.session.read()
|
|
418
420
|
if (view.saving) return false
|
|
419
421
|
if (!view.dirty) return true
|
|
420
|
-
|
|
421
|
-
The shell asks this while the edited model is still on screen. The app's own prompt, as every operato screen
|
|
422
|
-
asks -- not the browser's confirm box (user, 2026-09-24).
|
|
423
|
-
*/
|
|
424
|
-
/* A new figure the dock is opening waits for this answer, not against a clock (audit N4). */
|
|
425
|
-
const answered = askingToLeave()
|
|
426
|
-
this.discardApproved = !!(await OxPrompt.open({
|
|
422
|
+
return {
|
|
427
423
|
type: 'warning',
|
|
428
424
|
title: i18next.t('figure.text.unsaved-changes'),
|
|
429
425
|
text: i18next.t('figure.text.leave-without-saving'),
|
|
430
426
|
confirmButton: { text: i18next.t('figure.button.leave-without-saving') },
|
|
431
427
|
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async canDeactivate(signal?: AbortSignal): Promise<boolean> {
|
|
432
|
+
const answered = askingToLeave()
|
|
433
|
+
const approved = await super.canDeactivate(signal)
|
|
434
|
+
answered?.(approved)
|
|
435
|
+
return approved
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
discardAddressEdits(): void {
|
|
439
|
+
this.discardApproved = true
|
|
440
|
+
super.discardAddressEdits()
|
|
435
441
|
}
|
|
436
442
|
async pageUpdated(changes: Record<string, unknown>, lifecycle: { resourceId?: string }) {
|
|
437
443
|
if (!this.active) {
|