@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
|
@@ -5,31 +5,15 @@ import '@operato/input/ox-input-select-buttons.js';
|
|
|
5
5
|
import './figure-slot-layout-panel.js';
|
|
6
6
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
7
7
|
import { i18next, localize } from '@operato/i18n';
|
|
8
|
-
import {
|
|
9
|
-
import { acceptsOf,
|
|
8
|
+
import { figureFaceOverhangOf } from '@hatiolab/figure-model';
|
|
9
|
+
import { acceptsOf, AXES, canFasten, canFollow, followCommandOf, followableAxes, positionIsShare, choiceOf, followWordsOf, heldBy, followOf, shownWhenOf, visibilityControlsOf, fasteningOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionFormOf, motionFormRefusalOf, endPlaceKeyOf, motionJoint, placeFacesOf, placeIdFor, placesOn, rowAxesOf, rowBlockOf, fixedJointOf, rowOf, positionsWorkedOut, measureNow } from './links.js';
|
|
10
10
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js';
|
|
11
|
-
import './
|
|
12
|
-
import {
|
|
11
|
+
import { insideAlignmentOf } from './fasten-geometry.js';
|
|
12
|
+
import { fastenFormOf, fastenPlanOf, fastenSentenceOf } from './fasten-form.js';
|
|
13
13
|
import { HelpToggles } from './help.js';
|
|
14
14
|
import { groupMenu, GroupMenuStyles } from './group-menu.js';
|
|
15
15
|
import { jointAxisWord } from './joint-axis.js';
|
|
16
16
|
import { placeTerms } from '@things-factory/figure-viewer/dist-client/detail-facts.js';
|
|
17
|
-
const FACES = Object.keys(FIGURE_FACES);
|
|
18
|
-
/** The target list's entry for the plane the figure stands by. */
|
|
19
|
-
const PLANE = '__plane';
|
|
20
|
-
const freshFasten = () => ({
|
|
21
|
-
face: 'bottom',
|
|
22
|
-
toPlane: false,
|
|
23
|
-
target: '',
|
|
24
|
-
targetFace: 'top',
|
|
25
|
-
facing: 'meet',
|
|
26
|
-
gap: 0,
|
|
27
|
-
gapKept: undefined,
|
|
28
|
-
twist: 0,
|
|
29
|
-
align: {},
|
|
30
|
-
alignMm: {},
|
|
31
|
-
alignInput: {}
|
|
32
|
-
});
|
|
33
17
|
const freshMotion = () => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: '', travelPlus: 0 });
|
|
34
18
|
const freshRow = () => ({ axis: '', pitch: 200, fills: true, count: 5 });
|
|
35
19
|
const freshPlace = () => ({ role: 'grip', face: 'top', label: '', accepts: '', capacity: 1 });
|
|
@@ -43,7 +27,8 @@ const freshPlace = () => ({ role: 'grip', face: 'top', label: '', accepts: '', c
|
|
|
43
27
|
*
|
|
44
28
|
* The sections are dimensions, placement and joint, in that order. Every
|
|
45
29
|
* change is one figure-model authoring command, sent as `command` and run by the editor through the session — one
|
|
46
|
-
* step of the undo history.
|
|
30
|
+
* step of the undo history. The fastening is said here in one line; it is chosen in the fastening window
|
|
31
|
+
* (`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
|
|
47
32
|
* size the figure does not declare) are not offered, so the author is not sent into a refusal.
|
|
48
33
|
*/
|
|
49
34
|
let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
@@ -56,11 +41,12 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
56
41
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
57
42
|
this.section = 'shape';
|
|
58
43
|
/**
|
|
59
|
-
* Its tab is the one shown
|
|
60
|
-
*
|
|
44
|
+
* Its tab is the one shown, and no window over it speaks instead. The response's row speaks of what a command would
|
|
45
|
+
* draw, and the properties' says there is none: only the one in sight may, as the hidden one's silence would clear what
|
|
46
|
+
* the shown one draws. While the fastening window is open it alone speaks (the editor sets this false).
|
|
61
47
|
*/
|
|
62
48
|
this.active = true;
|
|
63
|
-
/** What the
|
|
49
|
+
/** What the row's command would do, in words (the editor works it out: `command-preview.ts`). */
|
|
64
50
|
this.previewWords = [];
|
|
65
51
|
/** Where the selected part's motion takes it, at both ends of its range (the editor draws them on the canvas). */
|
|
66
52
|
this.motionEnds = null;
|
|
@@ -71,15 +57,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
71
57
|
/** What taking this part off would do, said beside the buttons that do it (worked out on a copy by the editor). */
|
|
72
58
|
this.detachWords = [];
|
|
73
59
|
this.told = '';
|
|
74
|
-
|
|
75
|
-
this.working = 'fasten';
|
|
76
|
-
/** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
|
|
77
|
-
this.choosingFastening = false;
|
|
78
|
-
/** The fastening the part has, opened to be changed: until then the place group says it in one line. */
|
|
79
|
-
this.changingFastening = false;
|
|
80
|
-
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
81
|
-
this.spotlit = false;
|
|
82
|
-
this.fasten = freshFasten();
|
|
60
|
+
this.rowOpen = false;
|
|
83
61
|
/** Why 정렬·간격 고치기 moved nothing, said under it; empty once it moved the part or the part changed. */
|
|
84
62
|
this.fixNote = '';
|
|
85
63
|
this.motion = freshMotion();
|
|
@@ -134,7 +112,6 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
134
112
|
details[resize-choice] { margin: 0; padding: 0; border: 0; }
|
|
135
113
|
details[resize-choice] > div { display: grid; gap: 8px; padding: 8px 0; }
|
|
136
114
|
p[fasten-mode] { margin: 0; font-weight: 600; }
|
|
137
|
-
section[fastening][spotlit] { outline: 2px solid var(--md-sys-color-primary, #477b9e); outline-offset: 4px; border-radius: 6px; transition: outline-color 0.3s; }
|
|
138
115
|
details { margin-top: 8px; }
|
|
139
116
|
details[row-fold] { margin: 0; }
|
|
140
117
|
details[row-fold] > summary { padding: 0; color: var(--md-sys-color-on-surface); }
|
|
@@ -148,7 +125,6 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
148
125
|
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); }
|
|
149
126
|
div[overhang] p { margin: 0; }
|
|
150
127
|
ul[detach-words] { margin: 4px 0 10px; padding-left: 20px; display: grid; gap: 2px; }
|
|
151
|
-
span[fasten-summary] { opacity: 0.8; }
|
|
152
128
|
/* The difference a following measure keeps: under its choice, on the same line as its words. */
|
|
153
129
|
label[inline-plus] {
|
|
154
130
|
display: flex;
|
|
@@ -172,15 +148,10 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
172
148
|
this.fixNote = '';
|
|
173
149
|
if (this.asset && this.partId && this.partId !== this.loadedFor) {
|
|
174
150
|
this.loadedFor = this.partId;
|
|
175
|
-
/* A part's own fastening is not another part's choices: the form opens on it (walk 1, 2026-09-26). */
|
|
176
|
-
const own = this.revision && this.partId ? fasteningOf(this.revision, this.partId) : null;
|
|
177
|
-
this.fasten = own ? { ...freshFasten(), ...own } : freshFasten();
|
|
178
151
|
this.motion = freshMotion();
|
|
179
152
|
this.place = freshPlace();
|
|
180
153
|
this.row = freshRow();
|
|
181
154
|
this.editingMotion = false;
|
|
182
|
-
this.choosingFastening = false;
|
|
183
|
-
this.changingFastening = false;
|
|
184
155
|
}
|
|
185
156
|
}
|
|
186
157
|
updated() {
|
|
@@ -260,7 +231,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
260
231
|
: nothing}
|
|
261
232
|
${rowOf(asset, this.partId) ? html `<slot name="row"></slot>` : this.renderRow(asset)}
|
|
262
233
|
`;
|
|
263
|
-
this.candidate = this.
|
|
234
|
+
this.candidate = this.rowOpen && this.rowCandidate ? { action: this.rowCandidate } : undefined;
|
|
264
235
|
return response;
|
|
265
236
|
}
|
|
266
237
|
const shown = html `
|
|
@@ -268,7 +239,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
268
239
|
<h3>${i18next.t('figure.label.group-size')}</h3>
|
|
269
240
|
<slot name="size"></slot>
|
|
270
241
|
</section>
|
|
271
|
-
<section fastening
|
|
242
|
+
<section fastening>
|
|
272
243
|
<h3>
|
|
273
244
|
${i18next.t('figure.label.group-place')}${this.help.button('figure.text.fastening-explained')}
|
|
274
245
|
${groupMenu([{ label: i18next.t('figure.button.detach'), icon: 'link_off', disabled: this.locked || !held, run: () => this.command({ kind: 'detach', part: this.partId }) }])}
|
|
@@ -412,50 +383,36 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
412
383
|
</div>`;
|
|
413
384
|
})}</div></details>`;
|
|
414
385
|
}
|
|
415
|
-
/**
|
|
416
|
-
* "Open attachment settings": the fastening this part has now -- its face, what it is on, the gap -- in the form below,
|
|
417
|
-
* brought into view with the focus on it. The form is loaded again first: the part was fastened by "follow size,
|
|
418
|
-
* attached" while it was selected, so the form still held what it had before (designer 2026-09-27: it opened the
|
|
419
|
-
* holding places instead).
|
|
420
|
-
*/
|
|
421
|
-
/** The face the part sits on, in view with the focus on it: where the place across the face is dragged or keyed. */
|
|
422
386
|
/**
|
|
423
387
|
* 정렬·간격 고치기: the part moved inside the face it is fastened on, as one step -- the least move on each side it is
|
|
424
388
|
* out, or centred when it is wider than the face. When nothing of it can be moved, why is said under the button, not
|
|
425
|
-
* nothing at all (:3300, 2026-10-11: the button only scrolled to the drawing, which was already in view).
|
|
389
|
+
* nothing at all (:3300, 2026-10-11: the button only scrolled to the drawing, which was already in view). The command
|
|
390
|
+
* is the fastening window's for the same choice (`fasten-form.ts`), with only the place across the face changed.
|
|
426
391
|
*/
|
|
427
|
-
|
|
392
|
+
fixAlignment() {
|
|
393
|
+
const revision = this.revision;
|
|
394
|
+
if (!revision)
|
|
395
|
+
return;
|
|
396
|
+
const form = fastenFormOf(revision, this.partId);
|
|
428
397
|
/* 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. */
|
|
429
|
-
const own =
|
|
398
|
+
const own = figureFaceOverhangOf(revision.asset, this.partId, revision.inputs);
|
|
430
399
|
const fixed = own?.state === 'unknown'
|
|
431
400
|
? { cannot: 'unmeasured' }
|
|
432
401
|
: own?.state === 'out' || own?.state === 'possible'
|
|
433
|
-
? insideAlignmentOf(
|
|
402
|
+
? insideAlignmentOf(fastenPlanOf(revision, this.partId, form).view, form.align, form.alignMm)
|
|
434
403
|
: { cannot: 'inside' };
|
|
435
404
|
if ('cannot' in fixed) {
|
|
436
|
-
this.fixNote = i18next.t(`figure.text.fix-alignment-${fixed.cannot}`, { part: this.partId, on: target });
|
|
405
|
+
this.fixNote = i18next.t(`figure.text.fix-alignment-${fixed.cannot}`, { part: this.partId, on: form.target });
|
|
437
406
|
return;
|
|
438
407
|
}
|
|
439
408
|
this.fixNote = '';
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
/** The fastening form, open on this part and in view: for "fasten it first" from elsewhere. */
|
|
444
|
-
async showFastening() {
|
|
445
|
-
if (this.asset)
|
|
446
|
-
await this.openFastening(this.asset);
|
|
409
|
+
const action = fastenPlanOf(revision, this.partId, { ...form, align: fixed.align, alignMm: fixed.alignMm }).action;
|
|
410
|
+
if (action)
|
|
411
|
+
this.command(action);
|
|
447
412
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this.fasten
|
|
451
|
-
/* Asked for from elsewhere: the form itself, not the one-line summary of the fastening it has. */
|
|
452
|
-
this.changingFastening = true;
|
|
453
|
-
this.spotlit = true;
|
|
454
|
-
await this.updateComplete;
|
|
455
|
-
const section = this.renderRoot.querySelector('section[fastening]');
|
|
456
|
-
section?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
457
|
-
(section?.querySelector('select, input') ?? section?.querySelector('button'))?.focus({ preventScroll: true });
|
|
458
|
-
setTimeout(() => (this.spotlit = false), 1600);
|
|
413
|
+
/** The fastening window, asked of the editor: it opens over the canvas on what the part has now. */
|
|
414
|
+
openFastening() {
|
|
415
|
+
this.dispatchEvent(new CustomEvent('fasten-open', { bubbles: true, composed: true }));
|
|
459
416
|
}
|
|
460
417
|
/**
|
|
461
418
|
* What holds the part, said as its state -- 「아랫면을 바닥에 유지 중」 -- with the ways to change it: open the fastening,
|
|
@@ -508,245 +465,61 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
508
465
|
</div>`);
|
|
509
466
|
}
|
|
510
467
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
468
|
+
* The fastening in one line -- what, on which face, how -- and 「바꾸기」 to open the window where it is chosen (chief
|
|
469
|
+
* architect 2026-10-16; user 2026-10-17: the result here, the editing in a window with room). Taking it off is in the
|
|
470
|
+
* group's 「⋯」. What hangs over the face, and the way to fix it, stay here: they are about the fastening as it is now.
|
|
513
471
|
*/
|
|
514
|
-
aimAt(asset, f) {
|
|
515
|
-
const theirFaces = f.target ? facesOf(asset, f.target) : FACES;
|
|
516
|
-
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0];
|
|
517
|
-
const aim = f.toPlane ? 'plane' : f.target ? { part: f.target, face: targetFace } : undefined;
|
|
518
|
-
this.fasten = { ...f, ...(aim ? placeKeptOf(this.revision, this.partId, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) };
|
|
519
|
-
}
|
|
520
472
|
renderFastening(asset) {
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
473
|
+
const revision = this.revision;
|
|
474
|
+
const own = fasteningOf(revision, this.partId);
|
|
475
|
+
/* 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. */
|
|
476
|
+
if (own?.atOrigin)
|
|
525
477
|
return html `<div fastened fasten-at-origin>
|
|
526
478
|
<p>${this.heldWords(asset)}</p>
|
|
527
479
|
${this.renderDetachWords()}
|
|
528
480
|
</div>`;
|
|
529
|
-
}
|
|
530
|
-
const f = this.fasten;
|
|
531
|
-
/* Not a part that hangs from this one: fastening a frame to its own roller would close a loop. */
|
|
532
|
-
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one));
|
|
533
|
-
/* Shown, not dropped, and not choosable, with the reason (ADR-0097 §5.5): fastening it would close a loop. */
|
|
534
|
-
const loops = (one) => hangsFrom(asset, one, this.partId);
|
|
535
|
-
const planeWord = i18next.t('figure.label.resize-place-' + (asset.placement ?? 'floor'));
|
|
536
|
-
/* The faces are the ones each shape declares: a part is fastened by one of its outer faces, to any face of the target. */
|
|
537
|
-
const mineFaces = ownFacesOf(asset, this.partId, f.toPlane);
|
|
538
|
-
const theirFaces = f.target ? facesOf(asset, f.target) : FACES;
|
|
539
|
-
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0];
|
|
540
|
-
const aim = f.toPlane ? 'plane' : { part: f.target, face: targetFace };
|
|
541
|
-
/* 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). */
|
|
542
|
-
const mode = f.toPlane ? 'meet' : modeOf(f.face, f.facing);
|
|
543
|
-
const derived = f.toPlane || f.target ? myFaceFor(this.revision, this.partId, aim, mode) : undefined;
|
|
544
|
-
const myFace = derived ?? (mineFaces.includes(f.face) ? f.face : mineFaces[0]);
|
|
545
|
-
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist, f.alignInput) : undefined;
|
|
546
|
-
const action = f.toPlane || f.target
|
|
547
|
-
? {
|
|
548
|
-
kind: 'attach',
|
|
549
|
-
part: this.partId,
|
|
550
|
-
face: myFace,
|
|
551
|
-
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
552
|
-
...(f.toPlane ? {} : { facing: f.facing }),
|
|
553
|
-
...fastenGapOf(f),
|
|
554
|
-
twist: f.twist,
|
|
555
|
-
...(align ? { align } : {}),
|
|
556
|
-
replace: !!held
|
|
557
|
-
}
|
|
558
|
-
: undefined;
|
|
559
|
-
if (action)
|
|
560
|
-
this.fastenCandidate = { action, ...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } }) };
|
|
561
|
-
/*
|
|
562
|
-
What the folded details hold that is not their default, said while folded; the way and the place on the face are
|
|
563
|
-
the drawing's, and said there (user 2026-10-02: the details had selects repeating the drawing). Nothing is said
|
|
564
|
-
for the default.
|
|
565
|
-
*/
|
|
566
|
-
const detail = [
|
|
567
|
-
...(f.gapKept !== undefined
|
|
568
|
-
? [i18next.t('figure.text.fasten-summary-gap-worked-out')]
|
|
569
|
-
: f.gap
|
|
570
|
-
? [i18next.t('figure.text.fasten-summary-gap', { mm: String(f.gap) })]
|
|
571
|
-
: []),
|
|
572
|
-
...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
|
|
573
|
-
];
|
|
574
|
-
/* 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. */
|
|
575
|
-
const view = f.toPlane || f.target ? faceViewOf(this.revision, this.partId, aim) : undefined;
|
|
576
|
-
const alignRows = f.toPlane || f.target ? alignRowsOf(asset, this.partId, aim, view, !!f.twist, f.alignInput) : [];
|
|
577
|
-
/* A fastening the part has is changed as soon as something is chosen, as the drawing does; a new one waits for 붙이기. */
|
|
578
|
-
const settle = () => this.onFastenRelease(!!held);
|
|
579
|
-
/*
|
|
580
|
-
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
|
|
581
|
-
said once, by the sentence over the drawing -- the panel's head says it too; a third and a fourth line saying the
|
|
582
|
-
same (「현재 관계 수정」, 「붙은 곳」) went (user 2026-10-01, the panel checked for balance).
|
|
583
|
-
*/
|
|
584
|
-
const current = asset.joints?.find(j => j.body1.part === this.partId);
|
|
585
|
-
const editing = i18next.t('figure.text.fasten-new');
|
|
586
|
-
/*
|
|
587
|
-
A fastening the part has, said in one line -- what, on which face, how -- and 「바꾸기」 to open the drawing and the
|
|
588
|
-
details (chief architect 2026-10-16). They were open on every fastened part, near a screen of the panel, for a
|
|
589
|
-
choice made once. Taking it off is in the group's 「⋯」. What would hang over still shows: it is about now.
|
|
590
|
-
*/
|
|
591
|
-
/* What hangs over the face, and the way to fix it: about the fastening as it is now, so said open or folded. */
|
|
592
481
|
const overhang = html `${this.overhangNote.length ? html `<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
593
482
|
${this.overhangWords.length
|
|
594
483
|
? html `<div overhang role="status">
|
|
595
484
|
${this.overhangWords.map(line => html `<p>${line}</p>`)}
|
|
596
|
-
${
|
|
597
|
-
? html `<button act fix-alignment ?disabled=${this.locked} @click=${() => this.fixAlignment(
|
|
485
|
+
${own
|
|
486
|
+
? html `<button act fix-alignment ?disabled=${this.locked} @click=${() => this.fixAlignment()}>
|
|
598
487
|
<md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}
|
|
599
488
|
</button>
|
|
600
489
|
${this.fixNote ? html `<p hint fix-note>${this.fixNote}</p>` : nothing}`
|
|
601
490
|
: nothing}
|
|
602
491
|
</div>`
|
|
603
492
|
: nothing}`;
|
|
604
|
-
if (
|
|
493
|
+
if (own && heldBy(asset, this.partId)) {
|
|
494
|
+
const form = fastenFormOf(revision, this.partId);
|
|
495
|
+
const plan = fastenPlanOf(revision, this.partId, form);
|
|
496
|
+
if (plan.action)
|
|
497
|
+
this.fastenCandidate = { action: plan.action, ...(plan.face ? { face: plan.face } : {}) };
|
|
498
|
+
const planeWord = i18next.t('figure.label.resize-place-' + (asset.placement ?? 'floor'));
|
|
605
499
|
return html `
|
|
606
500
|
${overhang}
|
|
607
501
|
<div fastened-summary>
|
|
608
|
-
<p fasten-sentence>${this.
|
|
609
|
-
<button act change-fastening ?disabled=${this.locked} @click=${() =>
|
|
502
|
+
<p fasten-sentence>${fastenSentenceOf(this.partId, form, plan, planeWord, (key, params) => i18next.t(key, params))}</p>
|
|
503
|
+
<button act change-fastening ?disabled=${this.locked} @click=${() => this.openFastening()}>${i18next.t('figure.button.change-fastening')}</button>
|
|
610
504
|
</div>
|
|
611
505
|
${this.renderDetachWords()}
|
|
612
506
|
`;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return html `
|
|
616
|
-
<p hint>${i18next.t('figure.text.fasten-start')}</p>
|
|
617
|
-
<button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
|
|
618
|
-
<md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
|
|
619
|
-
</button>
|
|
620
|
-
`;
|
|
507
|
+
}
|
|
508
|
+
/* A blank attachment is not a form to decipher: what it does, and the way to the window. */
|
|
621
509
|
return html `
|
|
622
|
-
${current ? nothing : html `<p fasten-mode>${editing}</p>`}
|
|
623
510
|
${overhang}
|
|
624
|
-
|
|
625
|
-
<
|
|
626
|
-
<
|
|
627
|
-
|
|
628
|
-
const value = e.target.value;
|
|
629
|
-
this.aimAt(asset, value === PLANE ? { ...f, toPlane: true, target: '' } : { ...f, toPlane: false, target: value });
|
|
630
|
-
if (value)
|
|
631
|
-
settle();
|
|
632
|
-
}}>
|
|
633
|
-
<option value="" ?selected=${!f.target && !f.toPlane}>${i18next.t('figure.label.target-part-placeholder')}</option>
|
|
634
|
-
<option value=${PLANE} ?selected=${f.toPlane}>${planeWord}</option>
|
|
635
|
-
${targets.map(one => loops(one)
|
|
636
|
-
? html `<option value=${one} disabled>${i18next.t('figure.text.target-would-loop', { part: one, mine: this.partId })}</option>`
|
|
637
|
-
: html `<option value=${one} ?selected=${one === f.target && !f.toPlane}>${one}</option>`)}
|
|
638
|
-
</select>
|
|
639
|
-
</div>
|
|
640
|
-
${f.toPlane || f.target
|
|
641
|
-
? html `<p fasten-sentence>${this.sentenceOf(myFace, f.toPlane ? planeWord : `${f.target}의 ${i18next.t('figure.label.face-' + targetFace)}`, mode)}</p>
|
|
642
|
-
<figure-fasten-drawing
|
|
643
|
-
.target=${f.toPlane ? '' : f.target}
|
|
644
|
-
.planeWord=${planeWord}
|
|
645
|
-
.faces=${theirFaces}
|
|
646
|
-
.face=${targetFace}
|
|
647
|
-
.modes=${f.toPlane ? ['meet'] : ([...['meet', 'flush'], ...(turnsInPlace(asset, this.partId) ? ['centre'] : [])])}
|
|
648
|
-
.mode=${mode}
|
|
649
|
-
.view=${view}
|
|
650
|
-
.align=${f.align}
|
|
651
|
-
.alignMm=${f.alignMm}
|
|
652
|
-
.gap=${f.gap}
|
|
653
|
-
.partName=${this.partId}
|
|
654
|
-
?locked=${this.locked}
|
|
655
|
-
?said=${!!held}
|
|
656
|
-
@fasten-change=${(e) => this.onFastenChange(asset, e.detail)}
|
|
657
|
-
@fasten-release=${() => this.onFastenRelease(!!held)}
|
|
658
|
-
></figure-fasten-drawing>`
|
|
659
|
-
: html `<p hint>${i18next.t('figure.text.fasten-target-first')}</p>`}
|
|
660
|
-
${(f.toPlane || f.target) ? html `<details fasten-detail>
|
|
661
|
-
<summary>${i18next.t('figure.label.fasten-detail')}${detail.length ? html ` · <span fasten-summary>${detail.join(' · ')}</span>` : nothing}</summary>
|
|
662
|
-
${alignRows.length ? this.renderAlignment(alignRows, settle) : nothing}
|
|
663
|
-
<div form>
|
|
664
|
-
<span>${i18next.t('figure.label.gap')}</span>
|
|
665
|
-
<input
|
|
666
|
-
type="number"
|
|
667
|
-
step="any"
|
|
668
|
-
.value=${f.gapKept !== undefined ? '' : String(f.gap)}
|
|
669
|
-
placeholder=${f.gapKept !== undefined ? i18next.t('figure.label.follows-expression') : ''}
|
|
670
|
-
@change=${(e) => ((this.fasten = { ...f, gap: Number(e.target.value) || 0, gapKept: undefined }), settle())}
|
|
671
|
-
/>
|
|
672
|
-
<span>${i18next.t('figure.label.twist')}</span>
|
|
673
|
-
<input type="number" step="any" .value=${String(f.twist)} @change=${(e) => ((this.fasten = { ...f, twist: Number(e.target.value) || 0 }), settle())} />
|
|
674
|
-
</div>
|
|
675
|
-
</details>
|
|
676
|
-
${ /* 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}
|
|
677
|
-
${held
|
|
678
|
-
? nothing
|
|
679
|
-
: html `<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
680
|
-
<md-icon>link</md-icon>${i18next.t('figure.button.fasten')}
|
|
681
|
-
</button>`}` : nothing}
|
|
511
|
+
<p hint>${i18next.t('figure.text.fasten-start')}</p>
|
|
512
|
+
<button act fasten-start ?disabled=${this.locked} @click=${() => this.openFastening()}>
|
|
513
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
|
|
514
|
+
</button>
|
|
682
515
|
`;
|
|
683
516
|
}
|
|
684
|
-
/** 「lid 의 아랫면을 bin 의 윗면에 맞닿게」: what the fastening is, in one line, the part's own face said as a result. */
|
|
685
|
-
sentenceOf(myFace, to, mode) {
|
|
686
|
-
return i18next.t(`figure.text.fasten-sentence-${mode}`, { part: this.partId, face: i18next.t('figure.label.face-' + myFace), to });
|
|
687
|
-
}
|
|
688
|
-
/** A change the drawing asks: to the form only; the preview behind follows the form. */
|
|
689
|
-
onFastenChange(asset, c) {
|
|
690
|
-
const f = this.fasten;
|
|
691
|
-
if ('targetFace' in c)
|
|
692
|
-
this.aimAt(asset, { ...f, targetFace: c.targetFace });
|
|
693
|
-
else if ('mode' in c)
|
|
694
|
-
this.fasten = { ...f, face: c.mode === 'centre' ? 'centre' : f.face === 'centre' ? 'bottom' : f.face, facing: c.mode === 'flush' ? 'flush' : 'meet' };
|
|
695
|
-
else if ('gap' in c)
|
|
696
|
-
this.fasten = { ...f, gap: c.gap, gapKept: undefined };
|
|
697
|
-
else
|
|
698
|
-
this.fasten = {
|
|
699
|
-
...f,
|
|
700
|
-
align: { ...f.align, [c.axis]: c.how },
|
|
701
|
-
alignMm: c.how === 'mm' ? { ...f.alignMm, [c.axis]: c.mm ?? 0 } : f.alignMm
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
/** Let go: a fastening the part already has is applied as it now is (one step); a new one waits for 붙이기. */
|
|
705
|
-
async onFastenRelease(held) {
|
|
706
|
-
if (!held)
|
|
707
|
-
return;
|
|
708
|
-
await this.updateComplete;
|
|
709
|
-
if (this.fastenCandidate)
|
|
710
|
-
this.command(this.fastenCandidate.action);
|
|
711
|
-
}
|
|
712
517
|
renderDetachWords() {
|
|
713
518
|
/* One thing to say is a line, not a list of one. */
|
|
714
519
|
if (this.detachWords.length === 1)
|
|
715
520
|
return html `<p hint detach-words>${this.detachWords[0]}</p>`;
|
|
716
521
|
return this.detachWords.length ? html `<ul detach-words>${this.detachWords.map(line => html `<li>${line}</li>`)}</ul>` : nothing;
|
|
717
522
|
}
|
|
718
|
-
/**
|
|
719
|
-
* Where on the target face the part sits, on the two axes across it: centred, level with the target's edge on
|
|
720
|
-
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
721
|
-
*/
|
|
722
|
-
renderAlignment(axes, settle) {
|
|
723
|
-
const f = this.fasten;
|
|
724
|
-
const set = (axis, how) => ((this.fasten = { ...f, align: { ...f.align, [axis]: how } }), settle());
|
|
725
|
-
const setMm = (axis, mm) => ((this.fasten = { ...f, alignMm: { ...f.alignMm, [axis]: mm } }), settle());
|
|
726
|
-
return html `<div form>
|
|
727
|
-
${axes.map(({ axis, edges }) => {
|
|
728
|
-
const how = f.align[axis] === 'min' || f.align[axis] === 'max' ? (edges ? f.align[axis] : 'centre') : (f.align[axis] ?? 'centre');
|
|
729
|
-
return html `<span>${i18next.t('figure.label.align-' + axis)}</span>
|
|
730
|
-
<select @change=${(e) => set(axis, e.target.value)}>
|
|
731
|
-
<option value="centre" ?selected=${how === 'centre'}>${i18next.t('figure.label.align-centre')}</option>
|
|
732
|
-
${edges
|
|
733
|
-
? html `<option value="min" ?selected=${how === 'min'}>${i18next.t(`figure.label.align-${axis}-min`)}</option>
|
|
734
|
-
<option value="max" ?selected=${how === 'max'}>${i18next.t(`figure.label.align-${axis}-max`)}</option>`
|
|
735
|
-
: nothing}
|
|
736
|
-
${f.alignInput?.[axis]
|
|
737
|
-
? html `<option value="input" ?selected=${how === 'input'}>${i18next.t('figure.label.align-input', { field: fieldWordsOf(f.alignInput[axis], key => i18next.t(key)) })}</option>`
|
|
738
|
-
: nothing}
|
|
739
|
-
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
740
|
-
${this.asset && hasInstanceSize(this.asset, axis)
|
|
741
|
-
? html `<option value="share" ?selected=${how === 'share'}>${i18next.t('figure.label.align-share')}</option>`
|
|
742
|
-
: nothing}
|
|
743
|
-
</select>
|
|
744
|
-
${how === 'mm' || how === 'share'
|
|
745
|
-
? html `<span></span><input type="number" step="any" aria-label=${i18next.t('figure.label.align-mm')} .value=${String(f.alignMm[axis] ?? 0)} @change=${(e) => setMm(axis, Number(e.target.value) || 0)} />`
|
|
746
|
-
: nothing}`;
|
|
747
|
-
})}
|
|
748
|
-
</div>`;
|
|
749
|
-
}
|
|
750
523
|
/**
|
|
751
524
|
* Making the part a row across what it is fastened to (figure-model `make-repeat`): along one of the axes across its
|
|
752
525
|
* face, copies a pitch apart filling the parent's length, or a count spread over it. Once made, the row's own section
|
|
@@ -778,7 +551,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
778
551
|
${head}
|
|
779
552
|
<p hint row-blocked>${i18next.t(`figure.text.row-blocked-${blocked ?? 'unfastened'}`, { part: this.partId })}</p>
|
|
780
553
|
${blocked === 'unfastened'
|
|
781
|
-
? html `<button act @click=${() => this.
|
|
554
|
+
? 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>`
|
|
782
555
|
: nothing}
|
|
783
556
|
</details></section>`;
|
|
784
557
|
const axis = axes.includes(r.axis) ? r.axis : axes[0];
|
|
@@ -787,13 +560,13 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
787
560
|
? { kind: 'make-repeat', part: this.partId, axis, pitch: r.fills ? r.pitch : Math.max(r.pitch, 1), ...(r.fills ? {} : { count: r.count }) }
|
|
788
561
|
: undefined;
|
|
789
562
|
this.rowCandidate = action;
|
|
790
|
-
const set = (next) => ((this.row = next), (this.
|
|
563
|
+
const set = (next) => ((this.row = next), (this.rowOpen = true));
|
|
791
564
|
/*
|
|
792
565
|
Whether the row is being worked follows the fold's own toggle. A press on the heading focuses it before the click;
|
|
793
566
|
set on focus, the fold opened there and the click then folded it again (2026-10-11: it took two clicks to open).
|
|
794
567
|
*/
|
|
795
|
-
const toggled = (e) => (this.
|
|
796
|
-
return html `<section row><details row-fold ?open=${this.
|
|
568
|
+
const toggled = (e) => (this.rowOpen = e.target.open);
|
|
569
|
+
return html `<section row><details row-fold ?open=${this.rowOpen} @toggle=${toggled}>
|
|
797
570
|
${head}
|
|
798
571
|
<div form>
|
|
799
572
|
<span>${i18next.t('figure.label.repeat-axis')}</span>
|
|
@@ -815,7 +588,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
815
588
|
<input type="number" step="1" min="1" .value=${String(r.count)} @change=${(e) => set({ ...r, count: Number(e.target.value) })} />
|
|
816
589
|
</div>
|
|
817
590
|
<p hint>${i18next.t('figure.text.row-count-spreads')}</p>`}
|
|
818
|
-
${action && this.
|
|
591
|
+
${action && this.rowOpen ? this.renderPreviewWords() : nothing}
|
|
819
592
|
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
820
593
|
<md-icon>view_week</md-icon>${i18next.t('figure.button.make-repeat')}
|
|
821
594
|
</button>
|
|
@@ -1057,26 +830,10 @@ __decorate([
|
|
|
1057
830
|
property({ attribute: false }),
|
|
1058
831
|
__metadata("design:type", Array)
|
|
1059
832
|
], FigureLinks.prototype, "detachWords", void 0);
|
|
1060
|
-
__decorate([
|
|
1061
|
-
state(),
|
|
1062
|
-
__metadata("design:type", String)
|
|
1063
|
-
], FigureLinks.prototype, "working", void 0);
|
|
1064
|
-
__decorate([
|
|
1065
|
-
state(),
|
|
1066
|
-
__metadata("design:type", Object)
|
|
1067
|
-
], FigureLinks.prototype, "choosingFastening", void 0);
|
|
1068
|
-
__decorate([
|
|
1069
|
-
state(),
|
|
1070
|
-
__metadata("design:type", Object)
|
|
1071
|
-
], FigureLinks.prototype, "changingFastening", void 0);
|
|
1072
|
-
__decorate([
|
|
1073
|
-
state(),
|
|
1074
|
-
__metadata("design:type", Object)
|
|
1075
|
-
], FigureLinks.prototype, "spotlit", void 0);
|
|
1076
833
|
__decorate([
|
|
1077
834
|
state(),
|
|
1078
835
|
__metadata("design:type", Object)
|
|
1079
|
-
], FigureLinks.prototype, "
|
|
836
|
+
], FigureLinks.prototype, "rowOpen", void 0);
|
|
1080
837
|
__decorate([
|
|
1081
838
|
state(),
|
|
1082
839
|
__metadata("design:type", Object)
|