@things-factory/figure-ui 10.1.85 → 10.1.87
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 +44 -3
- package/client/modeller/figure-section-editor.ts +643 -0
- package/client/modeller/figure-section-panel.ts +7 -0
- package/client/modeller/live.ts +5 -0
- package/client/modeller/section-drawing.ts +150 -0
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/editor.d.ts +6 -0
- package/dist-client/modeller/editor.js +50 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +87 -0
- package/dist-client/modeller/figure-section-editor.js +635 -0
- package/dist-client/modeller/figure-section-editor.js.map +1 -0
- package/dist-client/modeller/figure-section-panel.js +7 -0
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/live.js +7 -0
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +52 -0
- package/dist-client/modeller/section-drawing.js +123 -0
- package/dist-client/modeller/section-drawing.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/browser/acceptance-page.js +7 -0
- package/test/section-editor.test.ts +232 -0
- package/test/section-panel.test.ts +12 -9
- package/translations/en.json +34 -0
- package/translations/ja.json +34 -0
- package/translations/ko.json +34 -0
- package/translations/ms.json +34 -0
- package/translations/zh.json +34 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import { css, html, LitElement, nothing, svg } from 'lit';
|
|
3
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
4
|
+
import { live } from 'lit/directives/live.js';
|
|
5
|
+
import { i18next, localize } from '@operato/i18n';
|
|
6
|
+
import { figureCavitySectionOf, figureSectionSidesOf } from '@hatiolab/figure-model';
|
|
7
|
+
import { SidePanelStyles } from './side-panel-styles.js';
|
|
8
|
+
import { insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf } from './section-drawing.js';
|
|
9
|
+
import { END_FACE } from './section-controls.js';
|
|
10
|
+
/** How the section is laid on the screen, by the axis it is pushed along: across to the right, then down. */
|
|
11
|
+
const SCREEN = { y: ['x', 'z'], x: ['z', 'y'], z: ['x', 'y'] };
|
|
12
|
+
/** The editor's width and room for the solid beside it: below this the two take turns. */
|
|
13
|
+
export const NARROW_BELOW = 900;
|
|
14
|
+
/**
|
|
15
|
+
* 단면 편집 — a pushed part's section and length in one editor (ADR-0100 §3.2, stage 2).
|
|
16
|
+
*
|
|
17
|
+
* The outline is drawn flat, looking along the push, in millimetres from the part's origin; the figure behind stays in
|
|
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.
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
let FigureSectionEditor = class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
31
|
+
constructor() {
|
|
32
|
+
super(...arguments);
|
|
33
|
+
/** The figure's revision now; the editor remembers the one it opened at. */
|
|
34
|
+
this.revision = 0;
|
|
35
|
+
/** Narrow: the outline or the solid behind. */
|
|
36
|
+
this.show = 'outline';
|
|
37
|
+
/** Set when the space the editor stands in leaves too little beside it to see the solid. */
|
|
38
|
+
this.narrow = false;
|
|
39
|
+
this.room = new ResizeObserver(([entry]) => {
|
|
40
|
+
this.narrow = (entry?.contentRect.width ?? 0) < NARROW_BELOW;
|
|
41
|
+
if (!this.narrow)
|
|
42
|
+
this.show = 'outline';
|
|
43
|
+
});
|
|
44
|
+
this.working = { points: [], across: {}, length: 0 };
|
|
45
|
+
this.selected = null;
|
|
46
|
+
this.note = '';
|
|
47
|
+
this.history = [];
|
|
48
|
+
this.openedAt = 0;
|
|
49
|
+
this.dragging = null;
|
|
50
|
+
/** The state as it was when a drag began: kept once something moves, so a press alone is not a step back. */
|
|
51
|
+
this.dragFrom = null;
|
|
52
|
+
this.extent = 200;
|
|
53
|
+
/*
|
|
54
|
+
* Heard first (capture on the window), and a key the editor takes goes no further: the scene behind listens too, and
|
|
55
|
+
* would take Esc as "select nothing" and Delete as "delete the part".
|
|
56
|
+
*/
|
|
57
|
+
this.keys = (e) => {
|
|
58
|
+
const inField = e.composedPath()[0]?.tagName === 'INPUT';
|
|
59
|
+
const take = () => (e.preventDefault(), e.stopImmediatePropagation());
|
|
60
|
+
if (e.key === 'Escape') {
|
|
61
|
+
take();
|
|
62
|
+
this.cancel();
|
|
63
|
+
}
|
|
64
|
+
else if ((e.key === 'Delete' || e.key === 'Backspace') && !inField && this.selected) {
|
|
65
|
+
/* In a field the key deletes a character; outside, the chosen point. */
|
|
66
|
+
take();
|
|
67
|
+
this.removePoint(this.selected);
|
|
68
|
+
}
|
|
69
|
+
else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'z' && !inField) {
|
|
70
|
+
take();
|
|
71
|
+
this.back();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
static { this.styles = [SidePanelStyles, css `
|
|
76
|
+
:host {
|
|
77
|
+
position: absolute;
|
|
78
|
+
left: 12px;
|
|
79
|
+
top: 12px;
|
|
80
|
+
bottom: 12px;
|
|
81
|
+
z-index: 5;
|
|
82
|
+
display: grid;
|
|
83
|
+
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
|
84
|
+
width: min(560px, calc(100% - 24px));
|
|
85
|
+
background: var(--md-sys-color-surface);
|
|
86
|
+
color: var(--md-sys-color-on-surface);
|
|
87
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
88
|
+
border-radius: 12px;
|
|
89
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
}
|
|
92
|
+
/* Each in its own row: the tabs are hidden on a wide screen, and without the rows named the body fell into theirs. */
|
|
93
|
+
header {
|
|
94
|
+
grid-row: 1;
|
|
95
|
+
}
|
|
96
|
+
nav[tabs] {
|
|
97
|
+
grid-row: 2;
|
|
98
|
+
}
|
|
99
|
+
div[body] {
|
|
100
|
+
grid-row: 3;
|
|
101
|
+
}
|
|
102
|
+
footer {
|
|
103
|
+
grid-row: 4;
|
|
104
|
+
}
|
|
105
|
+
header {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: baseline;
|
|
108
|
+
gap: 10px;
|
|
109
|
+
padding: 10px 14px 4px;
|
|
110
|
+
}
|
|
111
|
+
header h2 {
|
|
112
|
+
margin: 0;
|
|
113
|
+
font-size: var(--ui-t-body);
|
|
114
|
+
}
|
|
115
|
+
header span {
|
|
116
|
+
font-size: 0.72rem;
|
|
117
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
118
|
+
}
|
|
119
|
+
nav[tabs] {
|
|
120
|
+
display: none;
|
|
121
|
+
gap: 4px;
|
|
122
|
+
padding: 0 14px 6px;
|
|
123
|
+
}
|
|
124
|
+
nav[tabs] button,
|
|
125
|
+
footer button,
|
|
126
|
+
li button {
|
|
127
|
+
font: inherit;
|
|
128
|
+
font-size: 0.78rem;
|
|
129
|
+
min-height: 30px;
|
|
130
|
+
padding: 0 12px;
|
|
131
|
+
border-radius: 15px;
|
|
132
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
133
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
134
|
+
color: var(--md-sys-color-on-surface);
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
nav[tabs] button[on] {
|
|
138
|
+
background: var(--md-sys-color-secondary-container);
|
|
139
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
140
|
+
}
|
|
141
|
+
div[body] {
|
|
142
|
+
display: grid;
|
|
143
|
+
grid-template-rows: minmax(160px, 1fr) auto;
|
|
144
|
+
min-height: 0;
|
|
145
|
+
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
146
|
+
}
|
|
147
|
+
div[outline] {
|
|
148
|
+
position: relative;
|
|
149
|
+
min-height: 0;
|
|
150
|
+
}
|
|
151
|
+
svg {
|
|
152
|
+
width: 100%;
|
|
153
|
+
height: 100%;
|
|
154
|
+
display: block;
|
|
155
|
+
touch-action: none;
|
|
156
|
+
user-select: none;
|
|
157
|
+
}
|
|
158
|
+
div[caption] {
|
|
159
|
+
position: absolute;
|
|
160
|
+
left: 8px;
|
|
161
|
+
top: 6px;
|
|
162
|
+
font-size: 0.68rem;
|
|
163
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
164
|
+
pointer-events: none;
|
|
165
|
+
}
|
|
166
|
+
div[caption][last-valid] {
|
|
167
|
+
color: var(--md-sys-color-error);
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
}
|
|
170
|
+
aside {
|
|
171
|
+
max-height: 34vh;
|
|
172
|
+
overflow: auto;
|
|
173
|
+
padding: 8px 14px;
|
|
174
|
+
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
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
|
+
}
|
|
190
|
+
ol[points] {
|
|
191
|
+
list-style: none;
|
|
192
|
+
margin: 0;
|
|
193
|
+
padding: 0;
|
|
194
|
+
display: grid;
|
|
195
|
+
gap: 4px;
|
|
196
|
+
}
|
|
197
|
+
ol[points] li {
|
|
198
|
+
display: grid;
|
|
199
|
+
grid-template-columns: 2.2em minmax(0, 1fr) minmax(0, 1fr) auto;
|
|
200
|
+
gap: 6px;
|
|
201
|
+
align-items: center;
|
|
202
|
+
padding: 2px 4px;
|
|
203
|
+
border-radius: 6px;
|
|
204
|
+
}
|
|
205
|
+
ol[points] li[selected] {
|
|
206
|
+
background: var(--md-sys-color-secondary-container);
|
|
207
|
+
}
|
|
208
|
+
ol[points] li[marked] b {
|
|
209
|
+
color: var(--md-sys-color-error);
|
|
210
|
+
}
|
|
211
|
+
ol[points] input {
|
|
212
|
+
min-width: 0;
|
|
213
|
+
font: inherit;
|
|
214
|
+
}
|
|
215
|
+
ul[issues] {
|
|
216
|
+
margin: 8px 0 0;
|
|
217
|
+
padding: 0 0 0 16px;
|
|
218
|
+
color: var(--md-sys-color-error);
|
|
219
|
+
font-size: 0.76rem;
|
|
220
|
+
}
|
|
221
|
+
small {
|
|
222
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
223
|
+
}
|
|
224
|
+
footer {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 8px;
|
|
228
|
+
padding: 8px 14px;
|
|
229
|
+
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
230
|
+
}
|
|
231
|
+
footer span[grow] {
|
|
232
|
+
flex: 1;
|
|
233
|
+
font-size: 0.72rem;
|
|
234
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
235
|
+
}
|
|
236
|
+
footer span[grow][stale] {
|
|
237
|
+
color: var(--md-sys-color-error);
|
|
238
|
+
}
|
|
239
|
+
footer button[primary] {
|
|
240
|
+
background: var(--md-sys-color-primary);
|
|
241
|
+
color: var(--md-sys-color-on-primary);
|
|
242
|
+
border-color: transparent;
|
|
243
|
+
}
|
|
244
|
+
button:disabled {
|
|
245
|
+
opacity: 0.45;
|
|
246
|
+
cursor: default;
|
|
247
|
+
}
|
|
248
|
+
/* Too little room beside the editor for the solid: the outline and the solid take turns (§7). */
|
|
249
|
+
:host([narrow]) nav[tabs] {
|
|
250
|
+
display: flex;
|
|
251
|
+
}
|
|
252
|
+
:host([narrow][show='solid']) div[body] {
|
|
253
|
+
display: none;
|
|
254
|
+
}
|
|
255
|
+
:host([narrow][show='solid']) {
|
|
256
|
+
bottom: auto;
|
|
257
|
+
}
|
|
258
|
+
`]; }
|
|
259
|
+
get axis() {
|
|
260
|
+
return (this.body?.shape.extrusion?.axis ?? 'y');
|
|
261
|
+
}
|
|
262
|
+
get kind() {
|
|
263
|
+
return this.body?.shape.extrusion?.section.kind;
|
|
264
|
+
}
|
|
265
|
+
connectedCallback() {
|
|
266
|
+
super.connectedCallback();
|
|
267
|
+
this.working = workingOf(this.body.shape);
|
|
268
|
+
this.opened = this.working;
|
|
269
|
+
this.lastValid = this.working;
|
|
270
|
+
this.openedAt = this.revision;
|
|
271
|
+
this.extent = this.extentOf(this.working);
|
|
272
|
+
window.addEventListener('keydown', this.keys, true);
|
|
273
|
+
if (this.parentElement)
|
|
274
|
+
this.room.observe(this.parentElement);
|
|
275
|
+
}
|
|
276
|
+
disconnectedCallback() {
|
|
277
|
+
window.removeEventListener('keydown', this.keys, true);
|
|
278
|
+
this.room.disconnect();
|
|
279
|
+
super.disconnectedCallback();
|
|
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
|
+
}
|
|
287
|
+
get stale() {
|
|
288
|
+
return this.revision !== this.openedAt;
|
|
289
|
+
}
|
|
290
|
+
valid(w) {
|
|
291
|
+
return workingIssuesOf(this.body.shape, w).words.length === 0;
|
|
292
|
+
}
|
|
293
|
+
change(next) {
|
|
294
|
+
this.history.push(this.working);
|
|
295
|
+
this.working = next;
|
|
296
|
+
this.note = '';
|
|
297
|
+
if (this.valid(next))
|
|
298
|
+
this.lastValid = next;
|
|
299
|
+
this.preview();
|
|
300
|
+
}
|
|
301
|
+
back() {
|
|
302
|
+
const before = this.history.pop();
|
|
303
|
+
if (!before)
|
|
304
|
+
return;
|
|
305
|
+
this.working = before;
|
|
306
|
+
if (this.valid(before))
|
|
307
|
+
this.lastValid = before;
|
|
308
|
+
this.preview();
|
|
309
|
+
}
|
|
310
|
+
/** The solid behind shows the working state, or the last valid one while the working one is refused. */
|
|
311
|
+
preview() {
|
|
312
|
+
this.dispatchEvent(new CustomEvent('command-live', { detail: { action: this.action(this.lastValid) }, bubbles: true, composed: true }));
|
|
313
|
+
}
|
|
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
|
+
};
|
|
326
|
+
}
|
|
327
|
+
removePoint(id) {
|
|
328
|
+
const next = removePoint(this.working.points, id);
|
|
329
|
+
if ('refused' in next) {
|
|
330
|
+
this.note = i18next.t('figure.text.section-issue-too-few');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.selected = null;
|
|
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 } };
|
|
341
|
+
}
|
|
342
|
+
apply() {
|
|
343
|
+
if (this.stale || !this.valid(this.working))
|
|
344
|
+
return;
|
|
345
|
+
this.dispatchEvent(new CustomEvent('command', { detail: { action: this.action(this.working) }, bubbles: true, composed: true }));
|
|
346
|
+
this.close();
|
|
347
|
+
}
|
|
348
|
+
cancel() {
|
|
349
|
+
this.dispatchEvent(new CustomEvent('command-live', { detail: { action: null }, bubbles: true, composed: true }));
|
|
350
|
+
this.close();
|
|
351
|
+
}
|
|
352
|
+
close() {
|
|
353
|
+
this.dispatchEvent(new CustomEvent('section-editor-close', { bubbles: true, composed: true }));
|
|
354
|
+
}
|
|
355
|
+
svgPoint(e) {
|
|
356
|
+
const el = this.renderRoot.querySelector('svg');
|
|
357
|
+
const pt = el.createSVGPoint();
|
|
358
|
+
pt.x = e.clientX;
|
|
359
|
+
pt.y = e.clientY;
|
|
360
|
+
const p = pt.matrixTransform(el.getScreenCTM().inverse());
|
|
361
|
+
return { h: Math.round(p.x), v: Math.round(p.y) };
|
|
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
|
+
}
|
|
373
|
+
render() {
|
|
374
|
+
if (!this.body)
|
|
375
|
+
return nothing;
|
|
376
|
+
const { issues, words } = workingIssuesOf(this.body.shape, this.working);
|
|
377
|
+
return html `
|
|
378
|
+
<header>
|
|
379
|
+
<h2>${i18next.t('figure.label.section-editor')}</h2>
|
|
380
|
+
<span>${this.body.id}</span>
|
|
381
|
+
</header>
|
|
382
|
+
<nav tabs>
|
|
383
|
+
${['outline', 'solid'].map(one => html `<button ?on=${this.show === one} @click=${() => (this.show = one)}>${i18next.t(`figure.label.section-editor-${one}`)}</button>`)}
|
|
384
|
+
</nav>
|
|
385
|
+
<div body>
|
|
386
|
+
<div outline>${this.drawing(issues, words.length > 0)}</div>
|
|
387
|
+
<aside>${this.list(issues, words)}</aside>
|
|
388
|
+
</div>
|
|
389
|
+
<footer>
|
|
390
|
+
<button ?disabled=${!this.history.length} @click=${() => this.back()}>${i18next.t('figure.button.section-editor-back')}</button>
|
|
391
|
+
<span grow ?stale=${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')}
|
|
395
|
+
</span>
|
|
396
|
+
<button @click=${() => this.cancel()}>${i18next.t('figure.button.section-editor-cancel')}</button>
|
|
397
|
+
<button primary ?disabled=${this.stale || words.length > 0} @click=${() => this.apply()}>${i18next.t('figure.button.section-editor-apply')}</button>
|
|
398
|
+
</footer>
|
|
399
|
+
`;
|
|
400
|
+
}
|
|
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) {
|
|
414
|
+
const [h, v] = SCREEN[this.axis];
|
|
415
|
+
const e = this.extent;
|
|
416
|
+
const marked = markedBy(issues);
|
|
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) : [];
|
|
421
|
+
const at = (id) => pts.find(p => p.id === id);
|
|
422
|
+
const grid = Math.max(10, Math.pow(10, Math.floor(Math.log10(e))) / 2);
|
|
423
|
+
const lines = [];
|
|
424
|
+
for (let g = -Math.floor(e / grid) * grid; g <= e; g += grid)
|
|
425
|
+
lines.push(g);
|
|
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';
|
|
429
|
+
return html `<div caption ?last-valid=${invalid}>
|
|
430
|
+
${invalid ? i18next.t('figure.text.section-editor-last-valid') : i18next.t('figure.text.section-editor-units', { h: h.toUpperCase(), v: v.toUpperCase() })}
|
|
431
|
+
</div>
|
|
432
|
+
<svg
|
|
433
|
+
viewBox=${`${-e} ${-e} ${2 * e} ${2 * e}`}
|
|
434
|
+
role="img"
|
|
435
|
+
aria-label=${i18next.t('figure.label.section-editor')}
|
|
436
|
+
@pointermove=${(ev) => {
|
|
437
|
+
if (!this.dragging)
|
|
438
|
+
return;
|
|
439
|
+
if (this.dragFrom)
|
|
440
|
+
this.history.push(this.dragFrom), (this.dragFrom = null);
|
|
441
|
+
const p = this.svgPoint(ev);
|
|
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))
|
|
447
|
+
this.lastValid = this.working;
|
|
448
|
+
this.preview();
|
|
449
|
+
}}
|
|
450
|
+
@pointerup=${() => ((this.dragging = null), (this.dragFrom = null))}
|
|
451
|
+
@pointerleave=${() => ((this.dragging = null), (this.dragFrom = null))}
|
|
452
|
+
>
|
|
453
|
+
${svg `${lines.map(g => svg `<line x1=${g} y1=${-e} x2=${g} y2=${e} stroke="var(--md-sys-color-outline-variant)" stroke-width=${e / 400} opacity="0.5" />
|
|
454
|
+
<line x1=${-e} y1=${g} x2=${e} y2=${g} stroke="var(--md-sys-color-outline-variant)" stroke-width=${e / 400} opacity="0.5" />`)}
|
|
455
|
+
<line x1=${-e} y1="0" x2=${e} y2="0" stroke="var(--md-sys-color-outline)" stroke-width=${e / 250} />
|
|
456
|
+
<line x1="0" y1=${-e} x2="0" y2=${e} stroke="var(--md-sys-color-outline)" stroke-width=${e / 250} />
|
|
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>
|
|
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>
|
|
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)
|
|
467
|
+
: nothing}
|
|
468
|
+
${sides.map(s => {
|
|
469
|
+
const a = at(s.from), b = at(s.to);
|
|
470
|
+
const bad = marked.sides.has(s.id);
|
|
471
|
+
return svg `<line x1=${a[h]} y1=${a[v]} x2=${b[h]} y2=${b[v]}
|
|
472
|
+
stroke=${bad ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-primary)'} stroke-width=${e / (bad ? 90 : 130)}
|
|
473
|
+
stroke-dasharray=${bad ? `${e / 40} ${e / 80}` : ''} />
|
|
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"
|
|
475
|
+
@pointerdown=${(ev) => {
|
|
476
|
+
ev.stopPropagation();
|
|
477
|
+
const made = insertAfter(this.working.points, s.from, this.axis);
|
|
478
|
+
this.change({ ...this.working, points: made.points });
|
|
479
|
+
this.selected = made.id;
|
|
480
|
+
}}><title>${i18next.t('figure.text.section-editor-add-here', { a: pointLabel(s.from), b: pointLabel(s.to) })}</title></line>`;
|
|
481
|
+
})}
|
|
482
|
+
${pts.map(p => {
|
|
483
|
+
const bad = marked.points.has(p.id), on = this.selected === p.id;
|
|
484
|
+
return svg `<g point=${p.id} style="cursor: grab"
|
|
485
|
+
@pointerdown=${(ev) => {
|
|
486
|
+
ev.stopPropagation();
|
|
487
|
+
ev.target.setPointerCapture?.(ev.pointerId);
|
|
488
|
+
this.dragFrom = this.working;
|
|
489
|
+
this.selected = p.id;
|
|
490
|
+
this.dragging = p.id;
|
|
491
|
+
}}>
|
|
492
|
+
<circle cx=${p[h]} cy=${p[v]} r=${e / (on ? 28 : 36)} fill=${bad ? 'var(--md-sys-color-error)' : on ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-surface)'}
|
|
493
|
+
stroke=${bad ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-primary)'} stroke-width=${e / 250} />
|
|
494
|
+
<text x=${p[h] + e / 22} y=${p[v] - e / 22} font-size=${e / 16} font-weight="600"
|
|
495
|
+
fill=${bad ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-on-surface)'}>${pointLabel(p.id)}</text>
|
|
496
|
+
</g>`;
|
|
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
|
+
})}`}
|
|
516
|
+
</svg>`;
|
|
517
|
+
}
|
|
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) => {
|
|
531
|
+
const input = ev.target;
|
|
532
|
+
const value = Number(input.value);
|
|
533
|
+
if (input.value === '' || !Number.isFinite(value)) {
|
|
534
|
+
input.value = String(now);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
set(value);
|
|
538
|
+
}}
|
|
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}
|
|
587
|
+
${this.note ? html `<small role="status">${this.note}</small>` : nothing}
|
|
588
|
+
${words.length
|
|
589
|
+
? html `<ul issues role="alert">
|
|
590
|
+
${words.map(w => html `<li>${i18next.t(w.key, w.params)}</li>`)}
|
|
591
|
+
</ul>`
|
|
592
|
+
: nothing}`;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
__decorate([
|
|
596
|
+
property({ attribute: false }),
|
|
597
|
+
__metadata("design:type", Object)
|
|
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);
|
|
607
|
+
__decorate([
|
|
608
|
+
property({ type: Number }),
|
|
609
|
+
__metadata("design:type", Object)
|
|
610
|
+
], FigureSectionEditor.prototype, "revision", void 0);
|
|
611
|
+
__decorate([
|
|
612
|
+
property({ reflect: true }),
|
|
613
|
+
__metadata("design:type", String)
|
|
614
|
+
], FigureSectionEditor.prototype, "show", void 0);
|
|
615
|
+
__decorate([
|
|
616
|
+
property({ type: Boolean, reflect: true }),
|
|
617
|
+
__metadata("design:type", Object)
|
|
618
|
+
], FigureSectionEditor.prototype, "narrow", void 0);
|
|
619
|
+
__decorate([
|
|
620
|
+
state(),
|
|
621
|
+
__metadata("design:type", Object)
|
|
622
|
+
], FigureSectionEditor.prototype, "working", void 0);
|
|
623
|
+
__decorate([
|
|
624
|
+
state(),
|
|
625
|
+
__metadata("design:type", Object)
|
|
626
|
+
], FigureSectionEditor.prototype, "selected", void 0);
|
|
627
|
+
__decorate([
|
|
628
|
+
state(),
|
|
629
|
+
__metadata("design:type", Object)
|
|
630
|
+
], FigureSectionEditor.prototype, "note", void 0);
|
|
631
|
+
FigureSectionEditor = __decorate([
|
|
632
|
+
customElement('figure-section-editor')
|
|
633
|
+
], FigureSectionEditor);
|
|
634
|
+
export { FigureSectionEditor };
|
|
635
|
+
//# sourceMappingURL=figure-section-editor.js.map
|