@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.
@@ -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 drawn section edited point by point (ADR-0100 §3.2, stage 2).
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 points make (`command-live`, the same preview the section panel uses), so the 2D
11
- * and the 3D stand side by side. A point is dragged or typed; a side pressed gains a point in its middle; a point is
12
- * deleted by its button or the Delete key, never below three. What the kernel would refuse is marked on the outline and
13
- * said in the list; apply is held back, and the solid shows the last valid shape, which the caption names.
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
- * Apply sends one `command` (one step of the history); cancel or Esc sends nothing and lets the preview go. The undo
16
- * here is the editor's own, for the working points only. If the figure changes while the editor is open, the working
17
- * points are no longer of the figure as it is, and apply is refused until the editor is opened again.
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 points as they were when a drag began: kept once the point moves, so a press alone is not a step back. */
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 points, or the last valid ones while the working ones are refused. */
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 { extentOf, insertAfter, issueWords, issuesOf, markedBy, movePoint, pointLabel, removePoint } from './section-drawing.js';
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 drawn section edited point by point (ADR-0100 §3.2, stage 2).
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 points make (`command-live`, the same preview the section panel uses), so the 2D
18
- * and the 3D stand side by side. A point is dragged or typed; a side pressed gains a point in its middle; a point is
19
- * deleted by its button or the Delete key, never below three. What the kernel would refuse is marked on the outline and
20
- * said in the list; apply is held back, and the solid shows the last valid shape, which the caption names.
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
- * Apply sends one `command` (one step of the history); cancel or Esc sends nothing and lets the preview go. The undo
23
- * here is the editor's own, for the working points only. If the figure changes while the editor is open, the working
24
- * points are no longer of the figure as it is, and apply is refused until the editor is opened again.
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 points as they were when a drag began: kept once the point moves, so a press alone is not a step back. */
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
- const s = this.body?.shape.extrusion?.section;
248
- this.working = s?.kind === 'polygon' ? s.points.map(p => ({ ...p })) : [];
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, this.axis);
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 (!issuesOf(next, this.axis).length)
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 (!issuesOf(before, this.axis).length)
306
+ if (this.valid(before))
278
307
  this.lastValid = before;
279
308
  this.preview();
280
309
  }
281
- /** The solid behind shows the working points, or the last valid ones while the working ones are refused. */
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
- action(points) {
286
- return { kind: 'set-section-points', part: this.body.id, points: points.map(p => ({ ...p })) };
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 || issuesOf(this.working, this.axis).length)
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 = issuesOf(this.working, this.axis);
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} · ${i18next.t('figure.text.section-editor-length', { length: String(Math.round(length * 1000) / 1000) })}</span>
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 ? i18next.t('figure.text.section-editor-stale') : i18next.t('figure.text.section-editor-hint')}
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 || issues.length > 0} @click=${() => this.apply()}>${i18next.t('figure.button.section-editor-apply')}</button>
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
- drawing(issues) {
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 pts = this.working;
350
- const sides = pts.length >= 2 ? figureSectionSidesOf({ kind: 'polygon', points: pts }, this.axis) : [];
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 invalid = issues.length > 0;
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
- this.working = movePoint(this.working, this.dragging, { [h]: p.h, [v]: p.v });
371
- if (!issuesOf(this.working, this.axis).length)
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 && this.lastValid.length
385
- ? svg `<polygon points=${this.lastValid.map(p => `${p[h]},${p[v]}`).join(' ')} fill="none" stroke="var(--md-sys-color-outline)" stroke-dasharray=${`${e / 50} ${e / 70}`} stroke-width=${e / 250} />`
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
- list(issues) {
421
- const [h, v] = SCREEN[this.axis];
422
- const marked = markedBy(issues);
423
- return html `<ol points aria-label=${i18next.t('figure.label.section-editor-points')}>
424
- ${this.working.map(p => html `<li ?selected=${this.selected === p.id} ?marked=${marked.points.has(p.id)} @focusin=${() => (this.selected = p.id)}>
425
- <b>${pointLabel(p.id)}</b>
426
- ${[h, v].map(axis => html `<input
427
- type="number"
428
- step="any"
429
- inputmode="decimal"
430
- aria-label=${i18next.t('figure.label.section-editor-coordinate', { point: pointLabel(p.id), axis: axis.toUpperCase() })}
431
- .value=${live(String(p[axis] ?? ''))}
432
- @change=${(ev) => {
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(p[axis] ?? '');
534
+ input.value = String(now);
437
535
  return;
438
536
  }
439
- this.change(movePoint(this.working, p.id, { [axis]: value }));
537
+ set(value);
440
538
  }}
441
- />`)}
442
- <button
443
- ?disabled=${this.working.length <= 3}
444
- title=${this.working.length <= 3 ? i18next.t('figure.text.section-issue-too-few') : ''}
445
- aria-label=${i18next.t('figure.button.section-editor-remove', { point: pointLabel(p.id) })}
446
- @click=${() => this.removePoint(p.id)}
447
- >
448
- ${i18next.t('figure.button.section-editor-remove', { point: pointLabel(p.id) })}
449
- </button>
450
- </li>`)}
451
- </ol>
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
- ${issues.length
588
+ ${words.length
454
589
  ? html `<ul issues role="alert">
455
- ${issues.map(i => {
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", Array)
621
+ __metadata("design:type", Object)
482
622
  ], FigureSectionEditor.prototype, "working", void 0);
483
623
  __decorate([
484
624
  state(),