@things-factory/figure-ui 10.1.86 → 10.1.88
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.ts +8 -2
- package/client/modeller/figure-section-editor.ts +244 -85
- package/client/modeller/figure-section-panel.ts +10 -10
- package/client/modeller/section-drawing.ts +55 -2
- package/dist-client/modeller/editor.js +8 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +36 -11
- package/dist-client/modeller/figure-section-editor.js +215 -75
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +10 -10
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +20 -1
- package/dist-client/modeller/section-drawing.js +42 -1
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/section-editor.test.ts +63 -4
- package/translations/en.json +11 -1
- package/translations/ja.json +11 -1
- package/translations/ko.json +11 -1
- package/translations/ms.json +11 -1
- package/translations/zh.json +11 -1
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { LitElement, nothing } from 'lit';
|
|
2
|
-
import type { FigureEvaluatedShape } from '@hatiolab/figure-model';
|
|
2
|
+
import type { FigureEvaluatedShape, FigureSectionMeasure } from '@hatiolab/figure-model';
|
|
3
|
+
type Axis = 'x' | 'y' | 'z';
|
|
3
4
|
/** The editor's width and room for the solid beside it: below this the two take turns. */
|
|
4
5
|
export declare const NARROW_BELOW = 900;
|
|
5
6
|
declare const FigureSectionEditor_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
6
7
|
/**
|
|
7
|
-
* 단면 편집 — a
|
|
8
|
+
* 단면 편집 — a pushed part's section and length in one editor (ADR-0100 §3.2, stage 2).
|
|
8
9
|
*
|
|
9
10
|
* The outline is drawn flat, looking along the push, in millimetres from the part's origin; the figure behind stays in
|
|
10
|
-
* view and shows the solid the working
|
|
11
|
-
* and the 3D stand side by side. A point is dragged or typed
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* view and shows the solid the working state makes (`command-live`, the same preview the section panel uses), so the 2D
|
|
12
|
+
* and the 3D stand side by side. A drawn section's point is dragged or typed, a side pressed gains a point in its
|
|
13
|
+
* middle, a point is deleted, never below three. A rectangle's width and depth and an ellipse's diameters are dragged by
|
|
14
|
+
* their handles or typed; a circle's two are one. The push's length is typed, and the face it keeps is the one the
|
|
15
|
+
* part's placement or fastening keeps (§4.1: read here, changed elsewhere). A measure that follows another value is
|
|
16
|
+
* shown and not offered.
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* What the kernel would refuse is marked on the outline and said in the list; apply is held back, and the solid shows
|
|
19
|
+
* the last valid state, which the caption names. Apply sends one `set-section` command (one step of the history); cancel
|
|
20
|
+
* or Esc sends nothing and lets the preview go. The undo here is the editor's own. If the figure changes while the editor
|
|
21
|
+
* is open, apply is refused until it is opened again.
|
|
18
22
|
*/
|
|
19
23
|
export declare class FigureSectionEditor extends FigureSectionEditor_base {
|
|
20
24
|
static styles: import("lit").CSSResult[];
|
|
@@ -23,6 +27,13 @@ export declare class FigureSectionEditor extends FigureSectionEditor_base {
|
|
|
23
27
|
id: string;
|
|
24
28
|
shape: FigureEvaluatedShape;
|
|
25
29
|
};
|
|
30
|
+
/** Which of its measures can be set (figure-model's `figureSectionMeasuresOf`). */
|
|
31
|
+
measures?: {
|
|
32
|
+
measures: Record<Axis, FigureSectionMeasure>;
|
|
33
|
+
round: boolean;
|
|
34
|
+
};
|
|
35
|
+
/** Along the push, the face a length change keeps: 1 its start, -1 its end, 0 its centre; undefined when it cannot be said. */
|
|
36
|
+
kept?: -1 | 0 | 1;
|
|
26
37
|
/** The figure's revision now; the editor remembers the one it opened at. */
|
|
27
38
|
revision: number;
|
|
28
39
|
/** Narrow: the outline or the solid behind. */
|
|
@@ -33,30 +44,44 @@ export declare class FigureSectionEditor extends FigureSectionEditor_base {
|
|
|
33
44
|
private working;
|
|
34
45
|
private selected;
|
|
35
46
|
private note;
|
|
47
|
+
private opened;
|
|
36
48
|
private history;
|
|
37
49
|
private lastValid;
|
|
38
50
|
private openedAt;
|
|
39
51
|
private dragging;
|
|
40
|
-
/** The
|
|
52
|
+
/** The state as it was when a drag began: kept once something moves, so a press alone is not a step back. */
|
|
41
53
|
private dragFrom;
|
|
42
54
|
private extent;
|
|
43
55
|
private get axis();
|
|
56
|
+
private get kind();
|
|
44
57
|
connectedCallback(): void;
|
|
45
58
|
disconnectedCallback(): void;
|
|
59
|
+
/** How far the drawing reaches from the origin: the outline's farthest point with room around, in 50 mm steps. */
|
|
60
|
+
private extentOf;
|
|
46
61
|
private keys;
|
|
47
62
|
private get stale();
|
|
63
|
+
private valid;
|
|
48
64
|
private change;
|
|
49
65
|
private back;
|
|
50
|
-
/** The solid behind shows the working
|
|
66
|
+
/** The solid behind shows the working state, or the last valid one while the working one is refused. */
|
|
51
67
|
private preview;
|
|
68
|
+
/** One command for everything changed since the editor opened; a measure not changed is not sent (it may follow another). */
|
|
52
69
|
private action;
|
|
53
70
|
private removePoint;
|
|
71
|
+
/** A width across set: a circle's two are one. */
|
|
72
|
+
private across;
|
|
54
73
|
private apply;
|
|
55
74
|
private cancel;
|
|
56
75
|
private close;
|
|
57
76
|
private svgPoint;
|
|
77
|
+
/** Words for the face a length change keeps, by the part's relations; nothing when they cannot say. */
|
|
78
|
+
private keptWords;
|
|
58
79
|
render(): import("lit-html").TemplateResult<1> | typeof nothing;
|
|
80
|
+
/** A section drawn as the screen lays it: across to the right, then down. */
|
|
81
|
+
private outline;
|
|
59
82
|
private drawing;
|
|
83
|
+
/** A millimetre field: committed to the working state on change, a bad number put back. */
|
|
84
|
+
private field;
|
|
60
85
|
private list;
|
|
61
86
|
}
|
|
62
87
|
export {};
|
|
@@ -3,25 +3,29 @@ import { css, html, LitElement, nothing, svg } from 'lit';
|
|
|
3
3
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
4
4
|
import { live } from 'lit/directives/live.js';
|
|
5
5
|
import { i18next, localize } from '@operato/i18n';
|
|
6
|
-
import { figureSectionSidesOf } from '@hatiolab/figure-model';
|
|
6
|
+
import { figureCavitySectionOf, figureSectionSidesOf } from '@hatiolab/figure-model';
|
|
7
7
|
import { SidePanelStyles } from './side-panel-styles.js';
|
|
8
|
-
import {
|
|
8
|
+
import { insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf } from './section-drawing.js';
|
|
9
|
+
import { END_FACE } from './section-controls.js';
|
|
9
10
|
/** How the section is laid on the screen, by the axis it is pushed along: across to the right, then down. */
|
|
10
11
|
const SCREEN = { y: ['x', 'z'], x: ['z', 'y'], z: ['x', 'y'] };
|
|
11
12
|
/** The editor's width and room for the solid beside it: below this the two take turns. */
|
|
12
13
|
export const NARROW_BELOW = 900;
|
|
13
14
|
/**
|
|
14
|
-
* 단면 편집 — a
|
|
15
|
+
* 단면 편집 — a pushed part's section and length in one editor (ADR-0100 §3.2, stage 2).
|
|
15
16
|
*
|
|
16
17
|
* The outline is drawn flat, looking along the push, in millimetres from the part's origin; the figure behind stays in
|
|
17
|
-
* view and shows the solid the working
|
|
18
|
-
* and the 3D stand side by side. A point is dragged or typed
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* view and shows the solid the working state makes (`command-live`, the same preview the section panel uses), so the 2D
|
|
19
|
+
* and the 3D stand side by side. A drawn section's point is dragged or typed, a side pressed gains a point in its
|
|
20
|
+
* middle, a point is deleted, never below three. A rectangle's width and depth and an ellipse's diameters are dragged by
|
|
21
|
+
* their handles or typed; a circle's two are one. The push's length is typed, and the face it keeps is the one the
|
|
22
|
+
* part's placement or fastening keeps (§4.1: read here, changed elsewhere). A measure that follows another value is
|
|
23
|
+
* shown and not offered.
|
|
21
24
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* What the kernel would refuse is marked on the outline and said in the list; apply is held back, and the solid shows
|
|
26
|
+
* the last valid state, which the caption names. Apply sends one `set-section` command (one step of the history); cancel
|
|
27
|
+
* or Esc sends nothing and lets the preview go. The undo here is the editor's own. If the figure changes while the editor
|
|
28
|
+
* is open, apply is refused until it is opened again.
|
|
25
29
|
*/
|
|
26
30
|
let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
27
31
|
constructor() {
|
|
@@ -37,14 +41,13 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
37
41
|
if (!this.narrow)
|
|
38
42
|
this.show = 'outline';
|
|
39
43
|
});
|
|
40
|
-
this.working = [];
|
|
44
|
+
this.working = { points: [], across: {}, length: 0 };
|
|
41
45
|
this.selected = null;
|
|
42
46
|
this.note = '';
|
|
43
47
|
this.history = [];
|
|
44
|
-
this.lastValid = [];
|
|
45
48
|
this.openedAt = 0;
|
|
46
49
|
this.dragging = null;
|
|
47
|
-
/** The
|
|
50
|
+
/** The state as it was when a drag began: kept once something moves, so a press alone is not a step back. */
|
|
48
51
|
this.dragFrom = null;
|
|
49
52
|
this.extent = 200;
|
|
50
53
|
/*
|
|
@@ -170,6 +173,20 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
170
173
|
padding: 8px 14px;
|
|
171
174
|
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
172
175
|
}
|
|
176
|
+
div[sizes] {
|
|
177
|
+
display: grid;
|
|
178
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
179
|
+
gap: 6px;
|
|
180
|
+
margin-bottom: 8px;
|
|
181
|
+
}
|
|
182
|
+
label[mm] {
|
|
183
|
+
display: grid;
|
|
184
|
+
gap: 2px;
|
|
185
|
+
}
|
|
186
|
+
label[mm] input {
|
|
187
|
+
min-width: 0;
|
|
188
|
+
font: inherit;
|
|
189
|
+
}
|
|
173
190
|
ol[points] {
|
|
174
191
|
list-style: none;
|
|
175
192
|
margin: 0;
|
|
@@ -242,13 +259,16 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
242
259
|
get axis() {
|
|
243
260
|
return (this.body?.shape.extrusion?.axis ?? 'y');
|
|
244
261
|
}
|
|
262
|
+
get kind() {
|
|
263
|
+
return this.body?.shape.extrusion?.section.kind;
|
|
264
|
+
}
|
|
245
265
|
connectedCallback() {
|
|
246
266
|
super.connectedCallback();
|
|
247
|
-
|
|
248
|
-
this.
|
|
267
|
+
this.working = workingOf(this.body.shape);
|
|
268
|
+
this.opened = this.working;
|
|
249
269
|
this.lastValid = this.working;
|
|
250
270
|
this.openedAt = this.revision;
|
|
251
|
-
this.extent = extentOf(this.working
|
|
271
|
+
this.extent = this.extentOf(this.working);
|
|
252
272
|
window.addEventListener('keydown', this.keys, true);
|
|
253
273
|
if (this.parentElement)
|
|
254
274
|
this.room.observe(this.parentElement);
|
|
@@ -258,14 +278,23 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
258
278
|
this.room.disconnect();
|
|
259
279
|
super.disconnectedCallback();
|
|
260
280
|
}
|
|
281
|
+
/** How far the drawing reaches from the origin: the outline's farthest point with room around, in 50 mm steps. */
|
|
282
|
+
extentOf(w) {
|
|
283
|
+
const [h, v] = SCREEN[this.axis];
|
|
284
|
+
const far = Math.max(1, ...w.points.flatMap(p => [Math.abs(p[h] ?? 0), Math.abs(p[v] ?? 0)]), ...Object.values(w.across).map(a => (a ?? 0) / 2));
|
|
285
|
+
return Math.ceil((far * 1.4 + 40) / 50) * 50;
|
|
286
|
+
}
|
|
261
287
|
get stale() {
|
|
262
288
|
return this.revision !== this.openedAt;
|
|
263
289
|
}
|
|
290
|
+
valid(w) {
|
|
291
|
+
return workingIssuesOf(this.body.shape, w).words.length === 0;
|
|
292
|
+
}
|
|
264
293
|
change(next) {
|
|
265
294
|
this.history.push(this.working);
|
|
266
295
|
this.working = next;
|
|
267
296
|
this.note = '';
|
|
268
|
-
if (
|
|
297
|
+
if (this.valid(next))
|
|
269
298
|
this.lastValid = next;
|
|
270
299
|
this.preview();
|
|
271
300
|
}
|
|
@@ -274,28 +303,44 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
274
303
|
if (!before)
|
|
275
304
|
return;
|
|
276
305
|
this.working = before;
|
|
277
|
-
if (
|
|
306
|
+
if (this.valid(before))
|
|
278
307
|
this.lastValid = before;
|
|
279
308
|
this.preview();
|
|
280
309
|
}
|
|
281
|
-
/** The solid behind shows the working
|
|
310
|
+
/** The solid behind shows the working state, or the last valid one while the working one is refused. */
|
|
282
311
|
preview() {
|
|
283
312
|
this.dispatchEvent(new CustomEvent('command-live', { detail: { action: this.action(this.lastValid) }, bubbles: true, composed: true }));
|
|
284
313
|
}
|
|
285
|
-
|
|
286
|
-
|
|
314
|
+
/** One command for everything changed since the editor opened; a measure not changed is not sent (it may follow another). */
|
|
315
|
+
action(w) {
|
|
316
|
+
const o = this.opened;
|
|
317
|
+
const changed = (a, b) => Math.abs((a ?? 0) - (b ?? 0)) > 1e-9;
|
|
318
|
+
const across = Object.fromEntries(Object.entries(w.across).filter(([k, v]) => changed(v, o.across[k])));
|
|
319
|
+
return {
|
|
320
|
+
kind: 'set-section',
|
|
321
|
+
part: this.body.id,
|
|
322
|
+
...(this.kind === 'polygon' ? { points: w.points.map(p => ({ ...p })) } : {}),
|
|
323
|
+
...(Object.keys(across).length ? { across } : {}),
|
|
324
|
+
...(changed(w.length, o.length) ? { length: w.length } : {})
|
|
325
|
+
};
|
|
287
326
|
}
|
|
288
327
|
removePoint(id) {
|
|
289
|
-
const next = removePoint(this.working, id);
|
|
328
|
+
const next = removePoint(this.working.points, id);
|
|
290
329
|
if ('refused' in next) {
|
|
291
330
|
this.note = i18next.t('figure.text.section-issue-too-few');
|
|
292
331
|
return;
|
|
293
332
|
}
|
|
294
333
|
this.selected = null;
|
|
295
|
-
this.change(next);
|
|
334
|
+
this.change({ ...this.working, points: next });
|
|
335
|
+
}
|
|
336
|
+
/** A width across set: a circle's two are one. */
|
|
337
|
+
across(axis, value) {
|
|
338
|
+
const [u, v] = SCREEN[this.axis];
|
|
339
|
+
const both = this.measures?.round ? { [u]: value, [v]: value } : { [axis]: value };
|
|
340
|
+
return { ...this.working, across: { ...this.working.across, ...both } };
|
|
296
341
|
}
|
|
297
342
|
apply() {
|
|
298
|
-
if (this.stale ||
|
|
343
|
+
if (this.stale || !this.valid(this.working))
|
|
299
344
|
return;
|
|
300
345
|
this.dispatchEvent(new CustomEvent('command', { detail: { action: this.action(this.working) }, bubbles: true, composed: true }));
|
|
301
346
|
this.close();
|
|
@@ -315,45 +360,72 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
315
360
|
const p = pt.matrixTransform(el.getScreenCTM().inverse());
|
|
316
361
|
return { h: Math.round(p.x), v: Math.round(p.y) };
|
|
317
362
|
}
|
|
363
|
+
/** Words for the face a length change keeps, by the part's relations; nothing when they cannot say. */
|
|
364
|
+
keptWords() {
|
|
365
|
+
if (this.kept === undefined)
|
|
366
|
+
return '';
|
|
367
|
+
if (this.kept === 0)
|
|
368
|
+
return i18next.t('figure.text.section-editor-keeps-centre');
|
|
369
|
+
return i18next.t('figure.text.section-editor-keeps', {
|
|
370
|
+
face: i18next.t('figure.label.face-' + END_FACE(this.axis, this.kept === 1 ? 'start' : 'end'))
|
|
371
|
+
});
|
|
372
|
+
}
|
|
318
373
|
render() {
|
|
319
374
|
if (!this.body)
|
|
320
375
|
return nothing;
|
|
321
|
-
const issues =
|
|
322
|
-
const length = this.body.shape.extrusion ? this.body.shape.extrusion.end - this.body.shape.extrusion.start : 0;
|
|
376
|
+
const { issues, words } = workingIssuesOf(this.body.shape, this.working);
|
|
323
377
|
return html `
|
|
324
378
|
<header>
|
|
325
379
|
<h2>${i18next.t('figure.label.section-editor')}</h2>
|
|
326
|
-
<span>${this.body.id}
|
|
380
|
+
<span>${this.body.id}</span>
|
|
327
381
|
</header>
|
|
328
382
|
<nav tabs>
|
|
329
383
|
${['outline', 'solid'].map(one => html `<button ?on=${this.show === one} @click=${() => (this.show = one)}>${i18next.t(`figure.label.section-editor-${one}`)}</button>`)}
|
|
330
384
|
</nav>
|
|
331
385
|
<div body>
|
|
332
|
-
<div outline>${this.drawing(issues)}</div>
|
|
333
|
-
<aside>${this.list(issues)}</aside>
|
|
386
|
+
<div outline>${this.drawing(issues, words.length > 0)}</div>
|
|
387
|
+
<aside>${this.list(issues, words)}</aside>
|
|
334
388
|
</div>
|
|
335
389
|
<footer>
|
|
336
390
|
<button ?disabled=${!this.history.length} @click=${() => this.back()}>${i18next.t('figure.button.section-editor-back')}</button>
|
|
337
391
|
<span grow ?stale=${this.stale}>
|
|
338
|
-
${this.stale
|
|
392
|
+
${this.stale
|
|
393
|
+
? i18next.t('figure.text.section-editor-stale')
|
|
394
|
+
: i18next.t(this.kind === 'polygon' ? 'figure.text.section-editor-hint' : 'figure.text.section-editor-hint-sized')}
|
|
339
395
|
</span>
|
|
340
396
|
<button @click=${() => this.cancel()}>${i18next.t('figure.button.section-editor-cancel')}</button>
|
|
341
|
-
<button primary ?disabled=${this.stale ||
|
|
397
|
+
<button primary ?disabled=${this.stale || words.length > 0} @click=${() => this.apply()}>${i18next.t('figure.button.section-editor-apply')}</button>
|
|
342
398
|
</footer>
|
|
343
399
|
`;
|
|
344
400
|
}
|
|
345
|
-
|
|
401
|
+
/** A section drawn as the screen lays it: across to the right, then down. */
|
|
402
|
+
outline(section, attrs, centre = {}) {
|
|
403
|
+
const [h, v] = SCREEN[this.axis];
|
|
404
|
+
const ch = centre[h] ?? 0, cv = centre[v] ?? 0;
|
|
405
|
+
if (section.kind === 'polygon')
|
|
406
|
+
return svg `<polygon points=${section.points.map(p => `${p[h]},${p[v]}`).join(' ')} fill=${attrs.fill} stroke=${attrs.stroke} stroke-width=${attrs.width} stroke-dasharray=${attrs.dash ?? ''} />`;
|
|
407
|
+
if (section.kind === 'ellipse')
|
|
408
|
+
return svg `<ellipse cx=${ch} cy=${cv} rx=${section.radius[h]} ry=${section.radius[v]} fill=${attrs.fill} stroke=${attrs.stroke} stroke-width=${attrs.width} stroke-dasharray=${attrs.dash ?? ''} />`;
|
|
409
|
+
const w = section.size[h], d = section.size[v];
|
|
410
|
+
return svg `<rect x=${ch - w / 2} y=${cv - d / 2} width=${w} height=${d} rx=${section.corner?.[h] ?? 0} ry=${section.corner?.[v] ?? 0}
|
|
411
|
+
fill=${attrs.fill} stroke=${attrs.stroke} stroke-width=${attrs.width} stroke-dasharray=${attrs.dash ?? ''} />`;
|
|
412
|
+
}
|
|
413
|
+
drawing(issues, invalid) {
|
|
346
414
|
const [h, v] = SCREEN[this.axis];
|
|
347
415
|
const e = this.extent;
|
|
348
416
|
const marked = markedBy(issues);
|
|
349
|
-
const
|
|
350
|
-
const
|
|
417
|
+
const w = this.working;
|
|
418
|
+
const section = sectionOf(this.body.shape, w);
|
|
419
|
+
const pts = w.points;
|
|
420
|
+
const sides = this.kind === 'polygon' && pts.length >= 2 ? figureSectionSidesOf({ kind: 'polygon', points: pts }, this.axis) : [];
|
|
351
421
|
const at = (id) => pts.find(p => p.id === id);
|
|
352
422
|
const grid = Math.max(10, Math.pow(10, Math.floor(Math.log10(e))) / 2);
|
|
353
423
|
const lines = [];
|
|
354
424
|
for (let g = -Math.floor(e / grid) * grid; g <= e; g += grid)
|
|
355
425
|
lines.push(g);
|
|
356
|
-
const
|
|
426
|
+
const cavity = this.body.shape.cavity;
|
|
427
|
+
const inside = cavity && !invalid ? figureCavitySectionOf(section, this.axis, cavity.wall) : undefined;
|
|
428
|
+
const sized = (axis) => this.measures?.measures[axis] === 'editable';
|
|
357
429
|
return html `<div caption ?last-valid=${invalid}>
|
|
358
430
|
${invalid ? i18next.t('figure.text.section-editor-last-valid') : i18next.t('figure.text.section-editor-units', { h: h.toUpperCase(), v: v.toUpperCase() })}
|
|
359
431
|
</div>
|
|
@@ -367,8 +439,11 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
367
439
|
if (this.dragFrom)
|
|
368
440
|
this.history.push(this.dragFrom), (this.dragFrom = null);
|
|
369
441
|
const p = this.svgPoint(ev);
|
|
370
|
-
|
|
371
|
-
|
|
442
|
+
const handle = /^handle:(.)$/.exec(this.dragging);
|
|
443
|
+
this.working = handle
|
|
444
|
+
? this.across(handle[1], Math.max(1, 2 * Math.abs(handle[1] === h ? p.h : p.v)))
|
|
445
|
+
: { ...this.working, points: movePoint(this.working.points, this.dragging, { [h]: p.h, [v]: p.v }) };
|
|
446
|
+
if (this.valid(this.working))
|
|
372
447
|
this.lastValid = this.working;
|
|
373
448
|
this.preview();
|
|
374
449
|
}}
|
|
@@ -381,10 +456,15 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
381
456
|
<line x1="0" y1=${-e} x2="0" y2=${e} stroke="var(--md-sys-color-outline)" stroke-width=${e / 250} />
|
|
382
457
|
<text x=${e * 0.92} y=${-e * 0.02} font-size=${e / 18} fill="var(--md-sys-color-on-surface-variant)">${h.toUpperCase()}</text>
|
|
383
458
|
<text x=${e * 0.02} y=${e * 0.95} font-size=${e / 18} fill="var(--md-sys-color-on-surface-variant)">${v.toUpperCase()}</text>
|
|
384
|
-
${invalid
|
|
385
|
-
|
|
459
|
+
${invalid ? this.outline(sectionOf(this.body.shape, this.lastValid), { fill: 'none', stroke: 'var(--md-sys-color-outline)', width: e / 250, dash: `${e / 50} ${e / 70}` }) : nothing}
|
|
460
|
+
${this.outline(section, {
|
|
461
|
+
fill: invalid ? 'none' : 'color-mix(in srgb, var(--md-sys-color-primary) 14%, transparent)',
|
|
462
|
+
stroke: this.kind === 'polygon' ? 'none' : invalid ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-primary)',
|
|
463
|
+
width: e / 130
|
|
464
|
+
})}
|
|
465
|
+
${inside && 'section' in inside
|
|
466
|
+
? this.outline(inside.section, { fill: 'var(--md-sys-color-surface)', stroke: 'var(--md-sys-color-primary)', width: e / 250, dash: `${e / 60} ${e / 90}` }, inside.centre)
|
|
386
467
|
: nothing}
|
|
387
|
-
<polygon points=${pts.map(p => `${p[h]},${p[v]}`).join(' ')} fill=${invalid ? 'none' : 'color-mix(in srgb, var(--md-sys-color-primary) 14%, transparent)'} stroke="none" />
|
|
388
468
|
${sides.map(s => {
|
|
389
469
|
const a = at(s.from), b = at(s.to);
|
|
390
470
|
const bad = marked.sides.has(s.id);
|
|
@@ -394,8 +474,8 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
394
474
|
<line side=${s.id} x1=${a[h]} y1=${a[v]} x2=${b[h]} y2=${b[v]} stroke="transparent" stroke-width=${e / 25} style="cursor: copy"
|
|
395
475
|
@pointerdown=${(ev) => {
|
|
396
476
|
ev.stopPropagation();
|
|
397
|
-
const made = insertAfter(this.working, s.from, this.axis);
|
|
398
|
-
this.change(made.points);
|
|
477
|
+
const made = insertAfter(this.working.points, s.from, this.axis);
|
|
478
|
+
this.change({ ...this.working, points: made.points });
|
|
399
479
|
this.selected = made.id;
|
|
400
480
|
}}><title>${i18next.t('figure.text.section-editor-add-here', { a: pointLabel(s.from), b: pointLabel(s.to) })}</title></line>`;
|
|
401
481
|
})}
|
|
@@ -414,48 +494,100 @@ let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(Li
|
|
|
414
494
|
<text x=${p[h] + e / 22} y=${p[v] - e / 22} font-size=${e / 16} font-weight="600"
|
|
415
495
|
fill=${bad ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-on-surface)'}>${pointLabel(p.id)}</text>
|
|
416
496
|
</g>`;
|
|
417
|
-
})}
|
|
497
|
+
})}
|
|
498
|
+
${this.kind === 'polygon'
|
|
499
|
+
? nothing
|
|
500
|
+
: [h, v].filter(sized).map(axis => {
|
|
501
|
+
const half = (w.across[axis] ?? 0) / 2;
|
|
502
|
+
const cx = axis === h ? half : 0, cy = axis === h ? 0 : half;
|
|
503
|
+
return svg `<g handle=${axis} style=${`cursor: ${axis === h ? 'ew-resize' : 'ns-resize'}`}
|
|
504
|
+
@pointerdown=${(ev) => {
|
|
505
|
+
ev.stopPropagation();
|
|
506
|
+
ev.target.setPointerCapture?.(ev.pointerId);
|
|
507
|
+
this.dragFrom = this.working;
|
|
508
|
+
this.dragging = `handle:${axis}`;
|
|
509
|
+
}}>
|
|
510
|
+
<rect x=${cx - e / 40} y=${cy - e / 40} width=${e / 20} height=${e / 20} rx=${e / 120}
|
|
511
|
+
fill="var(--md-sys-color-surface)" stroke="var(--md-sys-color-primary)" stroke-width=${e / 200} />
|
|
512
|
+
<text x=${cx + e / 28} y=${cy - e / 28} font-size=${e / 18} fill="var(--md-sys-color-on-surface)">${Math.round((w.across[axis] ?? 0) * 10) / 10}</text>
|
|
513
|
+
<title>${i18next.t('figure.label.section-editor-across', { axis: axis.toUpperCase() })}</title>
|
|
514
|
+
</g>`;
|
|
515
|
+
})}`}
|
|
418
516
|
</svg>`;
|
|
419
517
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
518
|
+
/** A millimetre field: committed to the working state on change, a bad number put back. */
|
|
519
|
+
field(label, now, set, disabled = false, note = '') {
|
|
520
|
+
return html `<label mm>
|
|
521
|
+
<span>${label}</span>
|
|
522
|
+
<input
|
|
523
|
+
type="number"
|
|
524
|
+
step="any"
|
|
525
|
+
min="0"
|
|
526
|
+
inputmode="decimal"
|
|
527
|
+
aria-label=${label}
|
|
528
|
+
.value=${live(String(Math.round(now * 1000) / 1000))}
|
|
529
|
+
?disabled=${disabled}
|
|
530
|
+
@change=${(ev) => {
|
|
433
531
|
const input = ev.target;
|
|
434
532
|
const value = Number(input.value);
|
|
435
533
|
if (input.value === '' || !Number.isFinite(value)) {
|
|
436
|
-
input.value = String(
|
|
534
|
+
input.value = String(now);
|
|
437
535
|
return;
|
|
438
536
|
}
|
|
439
|
-
|
|
537
|
+
set(value);
|
|
440
538
|
}}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
539
|
+
/>
|
|
540
|
+
${note ? html `<small>${note}</small>` : nothing}
|
|
541
|
+
</label>`;
|
|
542
|
+
}
|
|
543
|
+
list(issues, words) {
|
|
544
|
+
const [h, v] = SCREEN[this.axis];
|
|
545
|
+
const marked = markedBy(issues);
|
|
546
|
+
const m = this.measures?.measures;
|
|
547
|
+
const linked = i18next.t('figure.text.section-editor-linked');
|
|
548
|
+
return html `<div sizes>
|
|
549
|
+
${this.field(i18next.t('figure.label.section-editor-length'), this.working.length, value => this.change({ ...this.working, length: value }), m?.[this.axis] !== 'editable', m?.[this.axis] === 'linked' ? linked : this.keptWords())}
|
|
550
|
+
${this.kind === 'polygon'
|
|
551
|
+
? nothing
|
|
552
|
+
: this.measures?.round
|
|
553
|
+
? this.field(i18next.t('figure.label.section-editor-diameter'), this.working.across[h] ?? 0, value => this.change(this.across(h, value)), m?.[h] !== 'editable', m?.[h] === 'linked' ? linked : '')
|
|
554
|
+
: [h, v].map(axis => this.field(i18next.t('figure.label.section-editor-across', { axis: axis.toUpperCase() }), this.working.across[axis] ?? 0, value => this.change(this.across(axis, value)), m?.[axis] !== 'editable', m?.[axis] === 'linked' ? linked : ''))}
|
|
555
|
+
</div>
|
|
556
|
+
${this.kind === 'polygon'
|
|
557
|
+
? html `<ol points aria-label=${i18next.t('figure.label.section-editor-points')}>
|
|
558
|
+
${this.working.points.map(p => html `<li ?selected=${this.selected === p.id} ?marked=${marked.points.has(p.id)} @focusin=${() => (this.selected = p.id)}>
|
|
559
|
+
<b>${pointLabel(p.id)}</b>
|
|
560
|
+
${[h, v].map(axis => html `<input
|
|
561
|
+
type="number"
|
|
562
|
+
step="any"
|
|
563
|
+
inputmode="decimal"
|
|
564
|
+
aria-label=${i18next.t('figure.label.section-editor-coordinate', { point: pointLabel(p.id), axis: axis.toUpperCase() })}
|
|
565
|
+
.value=${live(String(p[axis] ?? ''))}
|
|
566
|
+
@change=${(ev) => {
|
|
567
|
+
const input = ev.target;
|
|
568
|
+
const value = Number(input.value);
|
|
569
|
+
if (input.value === '' || !Number.isFinite(value)) {
|
|
570
|
+
input.value = String(p[axis] ?? '');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
this.change({ ...this.working, points: movePoint(this.working.points, p.id, { [axis]: value }) });
|
|
574
|
+
}}
|
|
575
|
+
/>`)}
|
|
576
|
+
<button
|
|
577
|
+
?disabled=${this.working.points.length <= 3}
|
|
578
|
+
title=${this.working.points.length <= 3 ? i18next.t('figure.text.section-issue-too-few') : ''}
|
|
579
|
+
aria-label=${i18next.t('figure.button.section-editor-remove', { point: pointLabel(p.id) })}
|
|
580
|
+
@click=${() => this.removePoint(p.id)}
|
|
581
|
+
>
|
|
582
|
+
${i18next.t('figure.button.section-editor-remove', { point: pointLabel(p.id) })}
|
|
583
|
+
</button>
|
|
584
|
+
</li>`)}
|
|
585
|
+
</ol>`
|
|
586
|
+
: nothing}
|
|
452
587
|
${this.note ? html `<small role="status">${this.note}</small>` : nothing}
|
|
453
|
-
${
|
|
588
|
+
${words.length
|
|
454
589
|
? html `<ul issues role="alert">
|
|
455
|
-
${
|
|
456
|
-
const w = issueWords(i, this.working, this.axis);
|
|
457
|
-
return html `<li>${i18next.t(w.key, w.params)}</li>`;
|
|
458
|
-
})}
|
|
590
|
+
${words.map(w => html `<li>${i18next.t(w.key, w.params)}</li>`)}
|
|
459
591
|
</ul>`
|
|
460
592
|
: nothing}`;
|
|
461
593
|
}
|
|
@@ -464,6 +596,14 @@ __decorate([
|
|
|
464
596
|
property({ attribute: false }),
|
|
465
597
|
__metadata("design:type", Object)
|
|
466
598
|
], FigureSectionEditor.prototype, "body", void 0);
|
|
599
|
+
__decorate([
|
|
600
|
+
property({ attribute: false }),
|
|
601
|
+
__metadata("design:type", Object)
|
|
602
|
+
], FigureSectionEditor.prototype, "measures", void 0);
|
|
603
|
+
__decorate([
|
|
604
|
+
property({ attribute: false }),
|
|
605
|
+
__metadata("design:type", Number)
|
|
606
|
+
], FigureSectionEditor.prototype, "kept", void 0);
|
|
467
607
|
__decorate([
|
|
468
608
|
property({ type: Number }),
|
|
469
609
|
__metadata("design:type", Object)
|
|
@@ -478,7 +618,7 @@ __decorate([
|
|
|
478
618
|
], FigureSectionEditor.prototype, "narrow", void 0);
|
|
479
619
|
__decorate([
|
|
480
620
|
state(),
|
|
481
|
-
__metadata("design:type",
|
|
621
|
+
__metadata("design:type", Object)
|
|
482
622
|
], FigureSectionEditor.prototype, "working", void 0);
|
|
483
623
|
__decorate([
|
|
484
624
|
state(),
|