@things-factory/figure-ui 10.1.84 → 10.1.86

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.
Files changed (40) hide show
  1. package/client/modeller/editor.ts +46 -2
  2. package/client/modeller/figure-parts.ts +4 -4
  3. package/client/modeller/figure-section-editor.ts +484 -0
  4. package/client/modeller/figure-section-panel.ts +211 -75
  5. package/client/modeller/live.ts +13 -4
  6. package/client/modeller/section-controls.ts +89 -8
  7. package/client/modeller/section-drawing.ts +97 -0
  8. package/dist-client/modeller/clips.d.ts +1 -1
  9. package/dist-client/modeller/editor.d.ts +8 -0
  10. package/dist-client/modeller/editor.js +51 -1
  11. package/dist-client/modeller/editor.js.map +1 -1
  12. package/dist-client/modeller/figure-parts.js +1 -3
  13. package/dist-client/modeller/figure-parts.js.map +1 -1
  14. package/dist-client/modeller/figure-section-editor.d.ts +62 -0
  15. package/dist-client/modeller/figure-section-editor.js +495 -0
  16. package/dist-client/modeller/figure-section-editor.js.map +1 -0
  17. package/dist-client/modeller/figure-section-panel.d.ts +26 -12
  18. package/dist-client/modeller/figure-section-panel.js +193 -69
  19. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  20. package/dist-client/modeller/live.d.ts +3 -1
  21. package/dist-client/modeller/live.js +13 -3
  22. package/dist-client/modeller/live.js.map +1 -1
  23. package/dist-client/modeller/section-controls.d.ts +33 -0
  24. package/dist-client/modeller/section-controls.js +71 -7
  25. package/dist-client/modeller/section-controls.js.map +1 -1
  26. package/dist-client/modeller/section-drawing.d.ts +33 -0
  27. package/dist-client/modeller/section-drawing.js +82 -0
  28. package/dist-client/modeller/section-drawing.js.map +1 -0
  29. package/dist-client/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +3 -3
  31. package/test/browser/acceptance-page.js +7 -0
  32. package/test/palette-icons.test.ts +1 -1
  33. package/test/parts-wiring.test.ts +6 -2
  34. package/test/section-editor.test.ts +192 -0
  35. package/test/section-panel.test.ts +108 -6
  36. package/translations/en.json +37 -2
  37. package/translations/ja.json +37 -2
  38. package/translations/ko.json +37 -2
  39. package/translations/ms.json +37 -2
  40. package/translations/zh.json +37 -2
@@ -1,29 +1,31 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import { css, html, LitElement, nothing } from 'lit';
3
- import { customElement, property } from 'lit/decorators.js';
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
6
  import { SidePanelStyles } from './side-panel-styles.js';
7
- import { cavityRequestOf, defaultWallOf, mm, sameOf } from './section-controls.js';
8
- /** The face each end of a push is, by the axis it runs along: its start is the low side. */
9
- const END_FACE = {
10
- x: { start: 'left', end: 'right' },
11
- y: { start: 'bottom', end: 'top' },
12
- z: { start: 'back', end: 'front' }
13
- };
7
+ import { END_FACE, SIDE_FACE, cavityDepthOf, cavityRefusalOf, cavityRequestOf, cavityResultOf, cornerLimitOf, cornerRefusalOf, defaultWallOf, mm, sameOf, sidesOf, sideWallLimitOf, wallLimitOf } from './section-controls.js';
14
8
  /**
15
- * 단면 — a pushed part's rounded corners and its inside (ADR-0100 stage 1).
9
+ * 단면 — a pushed part's rounded corners and its inside (ADR-0100 §3.3).
16
10
  *
17
- * Only what the part's section has: corners for a rectangle, an inside for a rectangle or an ellipse. A drawn section
18
- * and a turned body show nothing here. A radius or a wall entered sets every corner or every wall to it; the kernel
19
- * holds per-axis values, and when they differ the field says so and shows none of them.
11
+ * Corners for a rectangle; an inside for a rectangle or an ellipse, with its walls (one thickness, or one per side when
12
+ * asked for), its two ends open or closed, and what that makes of the part and how deep. A drawn section says beside
13
+ * the inside why it has none. A turned body shows nothing here.
20
14
  *
21
- * Every change leaves as one `command`; the editor runs it through the session, one step of the undo history.
15
+ * A value being typed is drawn at once and recorded nowhere (`command-live`); a value the kernel would refuse is said
16
+ * under its field with the most it can be, and is not sent. Enter or leaving the field sends the one `command`, one step
17
+ * of the undo history; Esc puts the value back.
22
18
  */
23
19
  let FigureSectionPanel = class FigureSectionPanel extends localize(i18next)(LitElement) {
24
20
  constructor() {
25
21
  super(...arguments);
26
22
  this.locked = false;
23
+ /** What each field would not take, by field, said under it. */
24
+ this.refusals = {};
25
+ /** What is typed in a field and not yet sent, by field: the figure drawn with it must not type over it. */
26
+ this.typed = {};
27
+ /** Walls one per side, asked for or because they already differ. */
28
+ this.perSide = false;
27
29
  }
28
30
  static { this.styles = [SidePanelStyles, css `
29
31
  :host {
@@ -38,21 +40,55 @@ let FigureSectionPanel = class FigureSectionPanel extends localize(i18next)(LitE
38
40
  flex: 1;
39
41
  min-width: 0;
40
42
  }
41
- div[row] > label {
42
- flex: 0 0 auto;
43
+ div[sides] {
44
+ display: grid;
45
+ grid-template-columns: repeat(2, minmax(0, 1fr));
46
+ gap: 6px;
47
+ /* A side's refusal under its field lengthens its cell only: the field beside it stays level. */
48
+ align-items: start;
49
+ }
50
+ div[sides] > label {
51
+ display: grid;
52
+ gap: 2px;
53
+ }
54
+ small[refusal] {
55
+ color: var(--md-sys-color-error);
56
+ }
57
+ input[aria-invalid='true'] {
58
+ outline: 2px solid var(--md-sys-color-error);
43
59
  }
44
60
  `]; }
61
+ willUpdate(changed) {
62
+ const before = changed.get('body');
63
+ if (changed.has('body') && before?.id !== this.body?.id) {
64
+ this.refusals = {};
65
+ this.typed = {};
66
+ /* A part whose walls already differ opens with them one per side. */
67
+ const walls = this.body?.shape.cavity?.wall;
68
+ this.perSide = !!walls && sameOf(Object.values(walls)) === undefined;
69
+ }
70
+ }
45
71
  render() {
46
72
  const body = this.body;
47
- const extrusion = body?.shape?.provider === 'extrude' ? body.shape.extrusion : undefined;
48
- if (!body || !extrusion)
49
- return nothing;
50
- const kind = extrusion.section.kind;
51
- if (kind !== 'rectangle' && kind !== 'ellipse')
73
+ const e = body?.shape?.provider === 'extrude' ? body.shape.extrusion : undefined;
74
+ if (!body || !e)
52
75
  return nothing;
76
+ const kind = e.section.kind;
53
77
  return html `<section>
54
78
  <h3>${i18next.t('figure.label.group-section')}</h3>
55
- ${kind === 'rectangle' ? this.renderCorner(body) : nothing} ${this.renderCavity(body, extrusion.axis)}
79
+ ${kind === 'rectangle' ? this.renderCorner(body) : nothing}
80
+ ${kind === 'rectangle' || kind === 'ellipse'
81
+ ? this.renderCavity(body, e.axis)
82
+ : html `${kind === 'polygon' && !body.shape.cavity && body.shape.dimensions.jamb === undefined
83
+ ? html `<div field>
84
+ <button act ?disabled=${this.locked} @click=${() => this.dispatchEvent(new CustomEvent('section-editor-open', { bubbles: true, composed: true }))}>
85
+ ${i18next.t('figure.button.section-editor-open')}
86
+ </button>
87
+ </div>`
88
+ : nothing}
89
+ <div field>
90
+ <small>${i18next.t('figure.text.section-cavity-unavailable')}</small>
91
+ </div>`}
56
92
  </section>`;
57
93
  }
58
94
  renderCorner(body) {
@@ -60,98 +96,174 @@ let FigureSectionPanel = class FigureSectionPanel extends localize(i18next)(LitE
60
96
  const reaches = Object.keys(d)
61
97
  .filter(k => k.startsWith('corner.'))
62
98
  .map(k => d[k]);
63
- const one = reaches.length ? sameOf(reaches) : 0;
99
+ const now = reaches.length ? sameOf(reaches) : 0;
100
+ const max = cornerLimitOf(body.shape);
64
101
  return html `<div field>
65
- <label>${i18next.t('figure.label.section-corner')}</label>
66
- <input
67
- type="number"
68
- min="0"
69
- step="any"
70
- .value=${live(one === undefined ? '' : String(mm(one)))}
71
- placeholder=${one === undefined ? i18next.t('figure.text.section-corner-differs') : ''}
72
- ?disabled=${this.locked}
73
- @change=${(e) => {
74
- const input = e.target;
75
- const value = Number(input.value);
76
- if (input.value === '' || !Number.isFinite(value) || value < 0) {
77
- input.value = one === undefined ? '' : String(mm(one));
78
- return;
79
- }
80
- /* 0 is a sharp corner: the rounding is taken away rather than kept at nothing. */
81
- this.command({ kind: 'set-section-corner', part: body.id, corner: value > 0 ? value : null });
82
- }}
83
- />
102
+ <label for="corner">${i18next.t('figure.label.section-corner')}</label>
103
+ ${this.number('corner', now, {
104
+ min: 0,
105
+ ask: value => {
106
+ const r = cornerRefusalOf(body.shape, value);
107
+ return r ? i18next.t('figure.text.section-corner-over', { max: String(mm(r.max)) }) : '';
108
+ },
109
+ action: value => ({ kind: 'set-section-corner', part: body.id, corner: value > 0 ? value : null }),
110
+ placeholder: now === undefined ? i18next.t('figure.text.section-corner-differs') : ''
111
+ })}
112
+ ${this.refusals.corner ? nothing : max !== undefined ? html `<small>${i18next.t('figure.text.section-at-most', { max: String(mm(max)) })}</small>` : nothing}
84
113
  </div>`;
85
114
  }
86
115
  renderCavity(body, axis) {
87
116
  const cavity = body.shape.cavity;
88
117
  const request = (patch) => cavityRequestOf(body.shape, patch);
118
+ const said = (ask) => !ask
119
+ ? ''
120
+ : ask.code === 'not-positive'
121
+ ? i18next.t('figure.text.section-not-positive')
122
+ : i18next.t(`figure.text.section-${ask.code === 'wall-too-thick' ? 'wall' : 'ends'}-over`, { max: String(ask.max) });
123
+ const cavityField = (key, now, patch) => this.number(key, now, {
124
+ min: 0,
125
+ ask: value => {
126
+ if (!(value > 0))
127
+ return i18next.t('figure.text.section-not-positive');
128
+ const ask = cavityRefusalOf(body.shape, request(patch(value)));
129
+ /* One side too thick: the most it can be is the width across less the wall opposite, not the even limit. */
130
+ const side = key.startsWith('wall.') ? key.slice(5) : undefined;
131
+ if (ask?.code === 'wall-too-thick' && side && cavity)
132
+ return said({ code: 'wall-too-thick', max: mm(sideWallLimitOf(body.shape, cavity.wall, side)) });
133
+ return said(ask);
134
+ },
135
+ action: value => ({ kind: 'set-cavity', part: body.id, cavity: request(patch(value)) })
136
+ });
89
137
  return html `<div field>
90
138
  <label>
91
139
  <input
92
140
  type="checkbox"
93
141
  .checked=${live(!!cavity)}
94
142
  ?disabled=${this.locked}
95
- @change=${(e) => {
96
- const on = e.target.checked;
97
- e.target.checked = !!cavity;
143
+ @change=${(ev) => {
144
+ const on = ev.target.checked;
145
+ ev.target.checked = !!cavity;
98
146
  this.command({ kind: 'set-cavity', part: body.id, cavity: on ? request({}) : null });
99
147
  }}
100
148
  />
101
149
  ${i18next.t('figure.label.section-cavity')}
102
150
  </label>
151
+ ${cavity
152
+ ? html `<small result>
153
+ ${i18next.t('figure.text.section-depth', {
154
+ result: i18next.t(`figure.label.section-result-${cavityResultOf(cavity)}`),
155
+ depth: String(cavityDepthOf(body.shape))
156
+ })}
157
+ </small>`
158
+ : nothing}
103
159
  </div>
104
160
  ${cavity
105
161
  ? html `<div field>
106
162
  <label>${i18next.t('figure.label.section-wall')}</label>
107
- ${this.thickness(sameOf(Object.values(cavity.wall)), value => this.command({ kind: 'set-cavity', part: body.id, cavity: request({ wall: value }) }), 'figure.text.section-wall-differs')}
163
+ ${this.perSide
164
+ ? html `<div sides>
165
+ ${[...sidesOf(axis)].sort((a, b) => a[0].localeCompare(b[0]) || (a[1] === '-' ? -1 : 1)).map(side => html `<label>
166
+ <span>${i18next.t('figure.label.face-' + SIDE_FACE[side])}</span>
167
+ ${cavityField(`wall.${side}`, cavity.wall[side], value => ({ wall: { ...cavity.wall, [side]: value } }))}
168
+ </label>`)}
169
+ </div>`
170
+ : cavityField('wall', sameOf(Object.values(cavity.wall)), value => ({ wall: value }))}
171
+ ${this.perSide || this.refusals.wall
172
+ ? nothing
173
+ : html `<small>${i18next.t('figure.text.section-wall-at-most', { max: String(mm(wallLimitOf(body.shape))) })}</small>`}
174
+ <label>
175
+ <input
176
+ type="checkbox"
177
+ .checked=${live(this.perSide)}
178
+ ?disabled=${this.locked}
179
+ @change=${(ev) => (this.perSide = ev.target.checked)}
180
+ />
181
+ ${i18next.t('figure.label.section-wall-per-side')}
182
+ </label>
108
183
  </div>
109
- ${['start', 'end'].map(end => this.renderEnd(body.id, axis, end, cavity[end], request))}`
184
+ ${['start', 'end'].map(end => this.renderEnd(body, axis, end, cavity[end], request, cavityField))}`
110
185
  : nothing}`;
111
186
  }
112
- renderEnd(part, axis, end, now, request) {
187
+ renderEnd(body, axis, end, now, request, cavityField) {
113
188
  const closed = now !== 'open';
114
- const set = (value) => this.command({ kind: 'set-cavity', part, cavity: request({ [end]: value }) });
115
189
  return html `<div field>
116
- <label>${i18next.t('figure.label.face-' + END_FACE[axis][end])}</label>
190
+ <label>${i18next.t('figure.label.face-' + END_FACE(axis, end))}</label>
117
191
  <div row>
118
192
  <select
119
193
  .value=${live(closed ? 'closed' : 'open')}
120
194
  ?disabled=${this.locked}
121
- @change=${(e) => {
122
- const select = e.target;
195
+ @change=${(ev) => {
196
+ const select = ev.target;
123
197
  const want = select.value;
124
198
  select.value = closed ? 'closed' : 'open';
125
- set(want === 'open' ? 'open' : { closed: defaultWallOf(this.body.shape) });
199
+ this.command({ kind: 'set-cavity', part: body.id, cavity: request({ [end]: want === 'open' ? 'open' : { closed: defaultWallOf(body.shape) } }) });
126
200
  }}
127
201
  >
128
202
  <option value="closed">${i18next.t('figure.label.section-end-closed')}</option>
129
203
  <option value="open">${i18next.t('figure.label.section-end-open')}</option>
130
204
  </select>
131
- ${closed ? this.thickness(now.closed, value => set({ closed: value })) : html `<span></span>`}
205
+ ${closed ? cavityField(`closed.${end}`, now.closed, value => ({ [end]: { closed: value } })) : html `<span></span>`}
132
206
  </div>
133
207
  </div>`;
134
208
  }
135
- /** A thickness field: a positive number, committed on change; anything else puts the old value back. */
136
- thickness(now, commit, differs) {
209
+ /**
210
+ * A millimetre field. Typing draws the value (or, if the kernel would refuse it, says why under the field and draws the
211
+ * figure as it stands); Enter or leaving sends it once; Esc puts the value back.
212
+ */
213
+ number(key, now, o) {
137
214
  const shown = now === undefined ? '' : String(mm(now));
215
+ const done = () => {
216
+ const { [key]: _, ...rest } = this.typed;
217
+ this.typed = rest;
218
+ };
219
+ const read = (input) => (input.value === '' ? NaN : Number(input.value));
220
+ const judge = (input) => {
221
+ const value = read(input);
222
+ const refusal = !Number.isFinite(value) || value < o.min ? '' : o.ask(value);
223
+ this.refusals = { ...this.refusals, [key]: refusal };
224
+ return Number.isFinite(value) && value >= o.min && !refusal ? value : undefined;
225
+ };
138
226
  return html `<input
139
- type="number"
140
- min="0"
141
- step="any"
142
- .value=${live(shown)}
143
- placeholder=${now === undefined && differs ? i18next.t(differs) : ''}
144
- ?disabled=${this.locked}
145
- @change=${(e) => {
146
- const input = e.target;
147
- const value = Number(input.value);
148
- if (input.value === '' || !Number.isFinite(value) || !(value > 0)) {
149
- input.value = shown;
227
+ id=${key}
228
+ type="number"
229
+ min=${o.min}
230
+ step="any"
231
+ inputmode="decimal"
232
+ .value=${live(this.typed[key] ?? shown)}
233
+ placeholder=${o.placeholder ?? ''}
234
+ aria-invalid=${this.refusals[key] ? 'true' : 'false'}
235
+ aria-describedby=${`${key}-refusal`}
236
+ ?disabled=${this.locked}
237
+ @input=${(ev) => {
238
+ this.typed = { ...this.typed, [key]: ev.target.value };
239
+ const value = judge(ev.target);
240
+ this.preview(value === undefined ? null : o.action(value));
241
+ }}
242
+ @change=${(ev) => {
243
+ const input = ev.target;
244
+ if (input.value === shown)
245
+ return done(), this.preview(null);
246
+ const value = judge(input);
247
+ /* A refused value stays typed with its reason under it, to be corrected; nothing is sent. */
248
+ if (value === undefined)
249
+ return this.preview(null);
250
+ done();
251
+ this.command(o.action(value));
252
+ }}
253
+ @keydown=${(ev) => {
254
+ if (ev.key !== 'Escape')
150
255
  return;
151
- }
152
- commit(value);
256
+ const input = ev.target;
257
+ input.value = shown;
258
+ done();
259
+ this.refusals = { ...this.refusals, [key]: '' };
260
+ this.preview(null);
153
261
  }}
154
- />`;
262
+ />
263
+ ${this.refusals[key] ? html `<small refusal id=${`${key}-refusal`} role="alert">${this.refusals[key]}</small>` : nothing}`;
264
+ }
265
+ preview(action) {
266
+ this.dispatchEvent(new CustomEvent('command-live', { detail: { action }, bubbles: true, composed: true }));
155
267
  }
156
268
  command(action) {
157
269
  this.dispatchEvent(new CustomEvent('command', { detail: { action }, bubbles: true, composed: true }));
@@ -165,6 +277,18 @@ __decorate([
165
277
  property({ type: Boolean }),
166
278
  __metadata("design:type", Object)
167
279
  ], FigureSectionPanel.prototype, "locked", void 0);
280
+ __decorate([
281
+ state(),
282
+ __metadata("design:type", Object)
283
+ ], FigureSectionPanel.prototype, "refusals", void 0);
284
+ __decorate([
285
+ state(),
286
+ __metadata("design:type", Object)
287
+ ], FigureSectionPanel.prototype, "typed", void 0);
288
+ __decorate([
289
+ state(),
290
+ __metadata("design:type", Object)
291
+ ], FigureSectionPanel.prototype, "perSide", void 0);
168
292
  FigureSectionPanel = __decorate([
169
293
  customElement('figure-section-panel')
170
294
  ], FigureSectionPanel);
@@ -1 +1 @@
1
- {"version":3,"file":"figure-section-panel.js","sourceRoot":"","sources":["../../client/modeller/figure-section-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { live } from 'lit/directives/live.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model'\nimport { SidePanelStyles } from './side-panel-styles.js'\nimport { cavityRequestOf, defaultWallOf, mm, sameOf } from './section-controls.js'\n\ntype Axis = 'x' | 'y' | 'z'\ntype End = 'start' | 'end'\n\n/** The face each end of a push is, by the axis it runs along: its start is the low side. */\nconst END_FACE: Record<Axis, Record<End, string>> = {\n x: { start: 'left', end: 'right' },\n y: { start: 'bottom', end: 'top' },\n z: { start: 'back', end: 'front' }\n}\n\n/**\n * 단면 — a pushed part's rounded corners and its inside (ADR-0100 stage 1).\n *\n * Only what the part's section has: corners for a rectangle, an inside for a rectangle or an ellipse. A drawn section\n * and a turned body show nothing here. A radius or a wall entered sets every corner or every wall to it; the kernel\n * holds per-axis values, and when they differ the field says so and shows none of them.\n *\n * Every change leaves as one `command`; the editor runs it through the session, one step of the undo history.\n */\n@customElement('figure-section-panel')\nexport class FigureSectionPanel extends localize(i18next)(LitElement) {\n static styles = [SidePanelStyles, css`\n :host {\n display: block;\n }\n div[row] {\n display: flex;\n gap: 6px;\n align-items: center;\n }\n div[row] > * {\n flex: 1;\n min-width: 0;\n }\n div[row] > label {\n flex: 0 0 auto;\n }\n `]\n\n @property({ attribute: false }) body?: { id: string; shape: FigureEvaluatedShape }\n @property({ type: Boolean }) locked = false\n\n render() {\n const body = this.body\n const extrusion = body?.shape?.provider === 'extrude' ? body.shape.extrusion : undefined\n if (!body || !extrusion) return nothing\n const kind = extrusion.section.kind\n if (kind !== 'rectangle' && kind !== 'ellipse') return nothing\n return html`<section>\n <h3>${i18next.t('figure.label.group-section')}</h3>\n ${kind === 'rectangle' ? this.renderCorner(body) : nothing} ${this.renderCavity(body, extrusion.axis)}\n </section>`\n }\n\n private renderCorner(body: { id: string; shape: FigureEvaluatedShape }) {\n const d = body.shape.dimensions\n const reaches = Object.keys(d)\n .filter(k => k.startsWith('corner.'))\n .map(k => d[k])\n const one = reaches.length ? sameOf(reaches) : 0\n return html`<div field>\n <label>${i18next.t('figure.label.section-corner')}</label>\n <input\n type=\"number\"\n min=\"0\"\n step=\"any\"\n .value=${live(one === undefined ? '' : String(mm(one)))}\n placeholder=${one === undefined ? i18next.t('figure.text.section-corner-differs') : ''}\n ?disabled=${this.locked}\n @change=${(e: Event) => {\n const input = e.target as HTMLInputElement\n const value = Number(input.value)\n if (input.value === '' || !Number.isFinite(value) || value < 0) {\n input.value = one === undefined ? '' : String(mm(one))\n return\n }\n /* 0 is a sharp corner: the rounding is taken away rather than kept at nothing. */\n this.command({ kind: 'set-section-corner', part: body.id, corner: value > 0 ? value : null })\n }}\n />\n </div>`\n }\n\n private renderCavity(body: { id: string; shape: FigureEvaluatedShape }, axis: Axis) {\n const cavity = body.shape.cavity\n const request = (patch: Partial<FigureCavityRequest>) => cavityRequestOf(body.shape, patch)\n return html`<div field>\n <label>\n <input\n type=\"checkbox\"\n .checked=${live(!!cavity)}\n ?disabled=${this.locked}\n @change=${(e: Event) => {\n const on = (e.target as HTMLInputElement).checked\n ;(e.target as HTMLInputElement).checked = !!cavity\n this.command({ kind: 'set-cavity', part: body.id, cavity: on ? request({}) : null })\n }}\n />\n ${i18next.t('figure.label.section-cavity')}\n </label>\n </div>\n ${cavity\n ? html`<div field>\n <label>${i18next.t('figure.label.section-wall')}</label>\n ${this.thickness(sameOf(Object.values(cavity.wall)), value => this.command({ kind: 'set-cavity', part: body.id, cavity: request({ wall: value }) }), 'figure.text.section-wall-differs')}\n </div>\n ${(['start', 'end'] as const).map(end => this.renderEnd(body.id, axis, end, cavity[end], request))}`\n : nothing}`\n }\n\n private renderEnd(\n part: string,\n axis: Axis,\n end: End,\n now: 'open' | { closed: number },\n request: (patch: Partial<FigureCavityRequest>) => FigureCavityRequest\n ) {\n const closed = now !== 'open'\n const set = (value: 'open' | { closed: number }) => this.command({ kind: 'set-cavity', part, cavity: request({ [end]: value }) })\n return html`<div field>\n <label>${i18next.t('figure.label.face-' + END_FACE[axis][end])}</label>\n <div row>\n <select\n .value=${live(closed ? 'closed' : 'open')}\n ?disabled=${this.locked}\n @change=${(e: Event) => {\n const select = e.target as HTMLSelectElement\n const want = select.value\n select.value = closed ? 'closed' : 'open'\n set(want === 'open' ? 'open' : { closed: defaultWallOf(this.body!.shape) })\n }}\n >\n <option value=\"closed\">${i18next.t('figure.label.section-end-closed')}</option>\n <option value=\"open\">${i18next.t('figure.label.section-end-open')}</option>\n </select>\n ${closed ? this.thickness(now.closed, value => set({ closed: value })) : html`<span></span>`}\n </div>\n </div>`\n }\n\n /** A thickness field: a positive number, committed on change; anything else puts the old value back. */\n private thickness(now: number | undefined, commit: (value: number) => void, differs?: string) {\n const shown = now === undefined ? '' : String(mm(now))\n return html`<input\n type=\"number\"\n min=\"0\"\n step=\"any\"\n .value=${live(shown)}\n placeholder=${now === undefined && differs ? i18next.t(differs) : ''}\n ?disabled=${this.locked}\n @change=${(e: Event) => {\n const input = e.target as HTMLInputElement\n const value = Number(input.value)\n if (input.value === '' || !Number.isFinite(value) || !(value > 0)) {\n input.value = shown\n return\n }\n commit(value)\n }}\n />`\n }\n\n private command(action: FigureAuthoringAction) {\n this.dispatchEvent(new CustomEvent('command', { detail: { action }, bubbles: true, composed: true }))\n }\n}\n"]}
1
+ {"version":3,"file":"figure-section-panel.js","sourceRoot":"","sources":["../../client/modeller/figure-section-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, nothing, type PropertyValues } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { live } from 'lit/directives/live.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model'\nimport { SidePanelStyles } from './side-panel-styles.js'\nimport {\n END_FACE,\n SIDE_FACE,\n cavityDepthOf,\n cavityRefusalOf,\n cavityRequestOf,\n cavityResultOf,\n cornerLimitOf,\n cornerRefusalOf,\n defaultWallOf,\n mm,\n sameOf,\n sidesOf,\n sideWallLimitOf,\n wallLimitOf,\n type FigureCavityAsk\n} from './section-controls.js'\n\ntype Axis = 'x' | 'y' | 'z'\ntype End = 'start' | 'end'\ntype Body = { id: string; shape: FigureEvaluatedShape }\n\n/**\n * 단면 — a pushed part's rounded corners and its inside (ADR-0100 §3.3).\n *\n * Corners for a rectangle; an inside for a rectangle or an ellipse, with its walls (one thickness, or one per side when\n * asked for), its two ends open or closed, and what that makes of the part and how deep. A drawn section says beside\n * the inside why it has none. A turned body shows nothing here.\n *\n * A value being typed is drawn at once and recorded nowhere (`command-live`); a value the kernel would refuse is said\n * under its field with the most it can be, and is not sent. Enter or leaving the field sends the one `command`, one step\n * of the undo history; Esc puts the value back.\n */\n@customElement('figure-section-panel')\nexport class FigureSectionPanel extends localize(i18next)(LitElement) {\n static styles = [SidePanelStyles, css`\n :host {\n display: block;\n }\n div[row] {\n display: flex;\n gap: 6px;\n align-items: center;\n }\n div[row] > * {\n flex: 1;\n min-width: 0;\n }\n div[sides] {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 6px;\n /* A side's refusal under its field lengthens its cell only: the field beside it stays level. */\n align-items: start;\n }\n div[sides] > label {\n display: grid;\n gap: 2px;\n }\n small[refusal] {\n color: var(--md-sys-color-error);\n }\n input[aria-invalid='true'] {\n outline: 2px solid var(--md-sys-color-error);\n }\n `]\n\n @property({ attribute: false }) body?: Body\n @property({ type: Boolean }) locked = false\n /** What each field would not take, by field, said under it. */\n @state() private refusals: Record<string, string> = {}\n /** What is typed in a field and not yet sent, by field: the figure drawn with it must not type over it. */\n @state() private typed: Record<string, string> = {}\n /** Walls one per side, asked for or because they already differ. */\n @state() private perSide = false\n\n protected willUpdate(changed: PropertyValues<this>) {\n const before = changed.get('body') as Body | undefined\n if (changed.has('body') && before?.id !== this.body?.id) {\n this.refusals = {}\n this.typed = {}\n /* A part whose walls already differ opens with them one per side. */\n const walls = this.body?.shape.cavity?.wall\n this.perSide = !!walls && sameOf(Object.values(walls)) === undefined\n }\n }\n\n render() {\n const body = this.body\n const e = body?.shape?.provider === 'extrude' ? body.shape.extrusion : undefined\n if (!body || !e) return nothing\n const kind = e.section.kind\n return html`<section>\n <h3>${i18next.t('figure.label.group-section')}</h3>\n ${kind === 'rectangle' ? this.renderCorner(body) : nothing}\n ${kind === 'rectangle' || kind === 'ellipse'\n ? this.renderCavity(body, e.axis)\n : html`${kind === 'polygon' && !body.shape.cavity && body.shape.dimensions.jamb === undefined\n ? html`<div field>\n <button act ?disabled=${this.locked} @click=${() => this.dispatchEvent(new CustomEvent('section-editor-open', { bubbles: true, composed: true }))}>\n ${i18next.t('figure.button.section-editor-open')}\n </button>\n </div>`\n : nothing}\n <div field>\n <small>${i18next.t('figure.text.section-cavity-unavailable')}</small>\n </div>`}\n </section>`\n }\n\n private renderCorner(body: Body) {\n const d = body.shape.dimensions\n const reaches = Object.keys(d)\n .filter(k => k.startsWith('corner.'))\n .map(k => d[k])\n const now = reaches.length ? sameOf(reaches) : 0\n const max = cornerLimitOf(body.shape)\n return html`<div field>\n <label for=\"corner\">${i18next.t('figure.label.section-corner')}</label>\n ${this.number('corner', now, {\n min: 0,\n ask: value => {\n const r = cornerRefusalOf(body.shape, value)\n return r ? i18next.t('figure.text.section-corner-over', { max: String(mm(r.max)) }) : ''\n },\n action: value => ({ kind: 'set-section-corner', part: body.id, corner: value > 0 ? value : null }),\n placeholder: now === undefined ? i18next.t('figure.text.section-corner-differs') : ''\n })}\n ${this.refusals.corner ? nothing : max !== undefined ? html`<small>${i18next.t('figure.text.section-at-most', { max: String(mm(max)) })}</small>` : nothing}\n </div>`\n }\n\n private renderCavity(body: Body, axis: Axis) {\n const cavity = body.shape.cavity\n const request = (patch: Partial<FigureCavityRequest>) => cavityRequestOf(body.shape, patch)\n const said = (ask: FigureCavityAsk | null) =>\n !ask\n ? ''\n : ask.code === 'not-positive'\n ? i18next.t('figure.text.section-not-positive')\n : i18next.t(`figure.text.section-${ask.code === 'wall-too-thick' ? 'wall' : 'ends'}-over`, { max: String(ask.max) })\n const cavityField = (key: string, now: number | undefined, patch: (value: number) => Partial<FigureCavityRequest>) =>\n this.number(key, now, {\n min: 0,\n ask: value => {\n if (!(value > 0)) return i18next.t('figure.text.section-not-positive')\n const ask = cavityRefusalOf(body.shape, request(patch(value)))\n /* One side too thick: the most it can be is the width across less the wall opposite, not the even limit. */\n const side = key.startsWith('wall.') ? key.slice(5) : undefined\n if (ask?.code === 'wall-too-thick' && side && cavity)\n return said({ code: 'wall-too-thick', max: mm(sideWallLimitOf(body.shape, cavity.wall, side)) })\n return said(ask)\n },\n action: value => ({ kind: 'set-cavity', part: body.id, cavity: request(patch(value)) })\n })\n return html`<div field>\n <label>\n <input\n type=\"checkbox\"\n .checked=${live(!!cavity)}\n ?disabled=${this.locked}\n @change=${(ev: Event) => {\n const on = (ev.target as HTMLInputElement).checked\n ;(ev.target as HTMLInputElement).checked = !!cavity\n this.command({ kind: 'set-cavity', part: body.id, cavity: on ? request({}) : null })\n }}\n />\n ${i18next.t('figure.label.section-cavity')}\n </label>\n ${cavity\n ? html`<small result>\n ${i18next.t('figure.text.section-depth', {\n result: i18next.t(`figure.label.section-result-${cavityResultOf(cavity)}`),\n depth: String(cavityDepthOf(body.shape))\n })}\n </small>`\n : nothing}\n </div>\n ${cavity\n ? html`<div field>\n <label>${i18next.t('figure.label.section-wall')}</label>\n ${this.perSide\n ? html`<div sides>\n ${[...sidesOf(axis)].sort((a, b) => a[0]!.localeCompare(b[0]!) || (a[1] === '-' ? -1 : 1)).map(\n side => html`<label>\n <span>${i18next.t('figure.label.face-' + SIDE_FACE[side])}</span>\n ${cavityField(`wall.${side}`, cavity.wall[side], value => ({ wall: { ...cavity.wall, [side]: value } }))}\n </label>`\n )}\n </div>`\n : cavityField('wall', sameOf(Object.values(cavity.wall)), value => ({ wall: value }))}\n ${this.perSide || this.refusals.wall\n ? nothing\n : html`<small>${i18next.t('figure.text.section-wall-at-most', { max: String(mm(wallLimitOf(body.shape))) })}</small>`}\n <label>\n <input\n type=\"checkbox\"\n .checked=${live(this.perSide)}\n ?disabled=${this.locked}\n @change=${(ev: Event) => (this.perSide = (ev.target as HTMLInputElement).checked)}\n />\n ${i18next.t('figure.label.section-wall-per-side')}\n </label>\n </div>\n ${(['start', 'end'] as const).map(end => this.renderEnd(body, axis, end, cavity[end], request, cavityField))}`\n : nothing}`\n }\n\n private renderEnd(\n body: Body,\n axis: Axis,\n end: End,\n now: 'open' | { closed: number },\n request: (patch: Partial<FigureCavityRequest>) => FigureCavityRequest,\n cavityField: (key: string, now: number | undefined, patch: (value: number) => Partial<FigureCavityRequest>) => unknown\n ) {\n const closed = now !== 'open'\n return html`<div field>\n <label>${i18next.t('figure.label.face-' + END_FACE(axis, end))}</label>\n <div row>\n <select\n .value=${live(closed ? 'closed' : 'open')}\n ?disabled=${this.locked}\n @change=${(ev: Event) => {\n const select = ev.target as HTMLSelectElement\n const want = select.value\n select.value = closed ? 'closed' : 'open'\n this.command({ kind: 'set-cavity', part: body.id, cavity: request({ [end]: want === 'open' ? 'open' : { closed: defaultWallOf(body.shape) } }) })\n }}\n >\n <option value=\"closed\">${i18next.t('figure.label.section-end-closed')}</option>\n <option value=\"open\">${i18next.t('figure.label.section-end-open')}</option>\n </select>\n ${closed ? cavityField(`closed.${end}`, now.closed, value => ({ [end]: { closed: value } })) : html`<span></span>`}\n </div>\n </div>`\n }\n\n /**\n * A millimetre field. Typing draws the value (or, if the kernel would refuse it, says why under the field and draws the\n * figure as it stands); Enter or leaving sends it once; Esc puts the value back.\n */\n private number(\n key: string,\n now: number | undefined,\n o: { min: number; ask: (value: number) => string; action: (value: number) => FigureAuthoringAction; placeholder?: string }\n ) {\n const shown = now === undefined ? '' : String(mm(now))\n const done = () => {\n const { [key]: _, ...rest } = this.typed\n this.typed = rest\n }\n const read = (input: HTMLInputElement) => (input.value === '' ? NaN : Number(input.value))\n const judge = (input: HTMLInputElement) => {\n const value = read(input)\n const refusal = !Number.isFinite(value) || value < o.min ? '' : o.ask(value)\n this.refusals = { ...this.refusals, [key]: refusal }\n return Number.isFinite(value) && value >= o.min && !refusal ? value : undefined\n }\n return html`<input\n id=${key}\n type=\"number\"\n min=${o.min}\n step=\"any\"\n inputmode=\"decimal\"\n .value=${live(this.typed[key] ?? shown)}\n placeholder=${o.placeholder ?? ''}\n aria-invalid=${this.refusals[key] ? 'true' : 'false'}\n aria-describedby=${`${key}-refusal`}\n ?disabled=${this.locked}\n @input=${(ev: Event) => {\n this.typed = { ...this.typed, [key]: (ev.target as HTMLInputElement).value }\n const value = judge(ev.target as HTMLInputElement)\n this.preview(value === undefined ? null : o.action(value))\n }}\n @change=${(ev: Event) => {\n const input = ev.target as HTMLInputElement\n if (input.value === shown) return done(), this.preview(null)\n const value = judge(input)\n /* A refused value stays typed with its reason under it, to be corrected; nothing is sent. */\n if (value === undefined) return this.preview(null)\n done()\n this.command(o.action(value))\n }}\n @keydown=${(ev: KeyboardEvent) => {\n if (ev.key !== 'Escape') return\n const input = ev.target as HTMLInputElement\n input.value = shown\n done()\n this.refusals = { ...this.refusals, [key]: '' }\n this.preview(null)\n }}\n />\n ${this.refusals[key] ? html`<small refusal id=${`${key}-refusal`} role=\"alert\">${this.refusals[key]}</small>` : nothing}`\n }\n\n private preview(action: FigureAuthoringAction | null) {\n this.dispatchEvent(new CustomEvent('command-live', { detail: { action }, bubbles: true, composed: true }))\n }\n\n private command(action: FigureAuthoringAction) {\n this.dispatchEvent(new CustomEvent('command', { detail: { action }, bubbles: true, composed: true }))\n }\n}\n"]}
@@ -1,10 +1,12 @@
1
- import type { FigureAsset, FigurePartEdit } from '@hatiolab/figure-model';
1
+ import type { FigureAsset, FigureAuthoringAction, FigurePartEdit } from '@hatiolab/figure-model';
2
2
  import { type FigureRenderPart } from './renderer.js';
3
3
  export interface LiveValues {
4
4
  /** Input values in hand, design or state, by input id. */
5
5
  values: Record<string, number>;
6
6
  /** A part edit in hand, not yet committed. */
7
7
  edit?: FigurePartEdit;
8
+ /** An authoring command in hand (a corner radius, an inside, being typed), not yet committed (ADR-0100 §3.3). */
9
+ action?: FigureAuthoringAction;
8
10
  }
9
11
  export declare const NOTHING_LIVE: LiveValues;
10
12
  /** Known inputs with a number, split by what they are. An id the figure does not declare is dropped. */
@@ -1,4 +1,4 @@
1
- import { compileFigureAsset, editFigurePart } from '@hatiolab/figure-model';
1
+ import { applyFigureAuthoring, compileFigureAsset, editFigurePart } from '@hatiolab/figure-model';
2
2
  import { drawableFigureParts } from './renderer.js';
3
3
  export const NOTHING_LIVE = { values: {} };
4
4
  /** Known inputs with a number, split by what they are. An id the figure does not declare is dropped. */
@@ -17,7 +17,7 @@ export function liveOverridesOf(asset, values) {
17
17
  return { design, state };
18
18
  }
19
19
  export function isLive(live) {
20
- return !!live.edit || Object.keys(live.values).length > 0;
20
+ return !!live.edit || !!live.action || Object.keys(live.values).length > 0;
21
21
  }
22
22
  /**
23
23
  * The bodies to draw with what is in hand, or null when nothing is -- then the evaluation as it stands is drawn.
@@ -29,9 +29,19 @@ export function isLive(live) {
29
29
  export function livePartsOf(asset, live) {
30
30
  if (!isLive(live))
31
31
  return null;
32
- const { design, state } = liveOverridesOf(asset, live.values);
33
32
  try {
34
33
  let next = live.edit ? editFigurePart(asset, live.edit) : asset;
34
+ if (live.action)
35
+ next = applyFigureAuthoring(next, live.action);
36
+ /* Read against the figure the edit makes: a command can take an input away (one wall becomes one per side). */
37
+ const { design, state } = liveOverridesOf(next, live.values);
38
+ /*
39
+ * An input the edit or the command in hand wrote takes its value: the values as they stand hold its old one (a
40
+ * point's coordinate before it was moved, a corner's radius before it was typed again) and would put it back.
41
+ */
42
+ for (const id of Object.keys(design))
43
+ if (next.designInputs[id] !== asset.designInputs[id])
44
+ delete design[id];
35
45
  if (Object.keys(design).length)
36
46
  next = { ...next, designInputs: { ...next.designInputs, ...design } };
37
47
  return drawableFigureParts(compileFigureAsset(next).evaluate(state).geometry);
@@ -1 +1 @@
1
- {"version":3,"file":"live.js","sourceRoot":"","sources":["../../client/modeller/live.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileFigureAsset, editFigurePart } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigurePartEdit } from '@hatiolab/figure-model'\n\nimport { drawableFigureParts, type FigureRenderPart } from './renderer.js'\n\n/*\n * The figure while a value is in hand: evaluated with the values being typed or dragged, drawn at once and written\n * nowhere. The panels committed on release only, so the canvas stood still while a number was typed, a spinner held,\n * a label scrubbed, a slider or the glow dragged (user, 2026-09-24: \"make the editor move naturally while the value\n * changes\"). The value in hand shows here; the release is the one step of the history.\n *\n * What is in hand is either input values -- a design value (a size, a position) or a pose -- or one part edit (the\n * glow of a material). A design value is a change of the figure's design, so it is shown by evaluating a copy with\n * that value; a pose is a state override, as the preview gives it.\n */\n\nexport interface LiveValues {\n /** Input values in hand, design or state, by input id. */\n values: Record<string, number>\n /** A part edit in hand, not yet committed. */\n edit?: FigurePartEdit\n}\n\nexport const NOTHING_LIVE: LiveValues = { values: {} }\n\n/** Known inputs with a number, split by what they are. An id the figure does not declare is dropped. */\nexport function liveOverridesOf(asset: FigureAsset, values: Record<string, number>): { design: Record<string, number>; state: Record<string, number> } {\n const roles = new Map(asset.document.model.inputs.map(i => [i.id, i.role]))\n const design: Record<string, number> = {},\n state: Record<string, number> = {}\n for (const [id, value] of Object.entries(values)) {\n if (!Number.isFinite(value)) continue\n const role = roles.get(id)\n if (role === 'state') state[id] = value\n else if (role !== undefined) design[id] = value\n }\n return { design, state }\n}\n\nexport function isLive(live: LiveValues): boolean {\n return !!live.edit || Object.keys(live.values).length > 0\n}\n\n/**\n * The bodies to draw with what is in hand, or null when nothing is -- then the evaluation as it stands is drawn.\n *\n * Also null when the figure cannot take the value in hand (a size of 0 on the way to typing 0.5, a glow the material\n * refuses): the canvas keeps the figure as it stands until the release, and the release reports why through the same\n * refusal every committed edit gets. A preview never invents a figure the commit would not make.\n */\nexport function livePartsOf(asset: FigureAsset, live: LiveValues): FigureRenderPart[] | null {\n if (!isLive(live)) return null\n const { design, state } = liveOverridesOf(asset, live.values)\n try {\n let next = live.edit ? editFigurePart(asset, live.edit) : asset\n if (Object.keys(design).length) next = { ...next, designInputs: { ...next.designInputs, ...design } }\n return drawableFigureParts(compileFigureAsset(next).evaluate(state).geometry)\n } catch {\n return null\n }\n}\n\n/**\n * How much one pixel of a label drag moves a value: a degree for an angle, a hundredth for a ratio, and for a length a\n * step that suits its size -- a millimetre under 100, ten over 1,000 -- so a 12 m wall and a 5 mm pin both scrub at a\n * usable pace. Shift makes it ten times finer.\n */\nexport function scrubStep(unit: string, value: number, fine = false): number {\n const base =\n unit === 'deg' ? 1 : unit === 'ratio' ? 0.01 : Math.max(1, Math.pow(10, Math.floor(Math.log10(Math.max(1, Math.abs(value)))) - 2))\n return fine ? base / 10 : base\n}\n\n/** A scrubbed value on the step's grid, so a drag does not leave 1203.0000000002 behind. */\nexport function scrubbed(start: number, pixels: number, step: number): number {\n const value = start + pixels * step\n const digits = Math.max(0, -Math.floor(Math.log10(step)))\n return Number(value.toFixed(digits))\n}\n\n/**\n * The bodies to draw while a value is in hand, over every input as the session holds it now (`current`): the pose the\n * author has already set with the other controls, and the design values. Drawing over the asset's saved defaults put\n * every other joint back to its saved angle while one slider moved (user, 2026-09-24: the arm jumped on release).\n */\nexport function liveOverSession(asset: FigureAsset, current: Record<string, number>, live: LiveValues): FigureRenderPart[] | null {\n if (!isLive(live)) return null\n return livePartsOf(asset, { ...live, values: { ...current, ...live.values } })\n}\n\n"]}
1
+ {"version":3,"file":"live.js","sourceRoot":"","sources":["../../client/modeller/live.ts"],"names":[],"mappings":"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","sourcesContent":["import { applyFigureAuthoring, compileFigureAsset, editFigurePart } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureAuthoringAction, FigurePartEdit } from '@hatiolab/figure-model'\n\nimport { drawableFigureParts, type FigureRenderPart } from './renderer.js'\n\n/*\n * The figure while a value is in hand: evaluated with the values being typed or dragged, drawn at once and written\n * nowhere. The panels committed on release only, so the canvas stood still while a number was typed, a spinner held,\n * a label scrubbed, a slider or the glow dragged (user, 2026-09-24: \"make the editor move naturally while the value\n * changes\"). The value in hand shows here; the release is the one step of the history.\n *\n * What is in hand is either input values -- a design value (a size, a position) or a pose -- or one part edit (the\n * glow of a material). A design value is a change of the figure's design, so it is shown by evaluating a copy with\n * that value; a pose is a state override, as the preview gives it.\n */\n\nexport interface LiveValues {\n /** Input values in hand, design or state, by input id. */\n values: Record<string, number>\n /** A part edit in hand, not yet committed. */\n edit?: FigurePartEdit\n /** An authoring command in hand (a corner radius, an inside, being typed), not yet committed (ADR-0100 §3.3). */\n action?: FigureAuthoringAction\n}\n\nexport const NOTHING_LIVE: LiveValues = { values: {} }\n\n/** Known inputs with a number, split by what they are. An id the figure does not declare is dropped. */\nexport function liveOverridesOf(asset: FigureAsset, values: Record<string, number>): { design: Record<string, number>; state: Record<string, number> } {\n const roles = new Map(asset.document.model.inputs.map(i => [i.id, i.role]))\n const design: Record<string, number> = {},\n state: Record<string, number> = {}\n for (const [id, value] of Object.entries(values)) {\n if (!Number.isFinite(value)) continue\n const role = roles.get(id)\n if (role === 'state') state[id] = value\n else if (role !== undefined) design[id] = value\n }\n return { design, state }\n}\n\nexport function isLive(live: LiveValues): boolean {\n return !!live.edit || !!live.action || Object.keys(live.values).length > 0\n}\n\n/**\n * The bodies to draw with what is in hand, or null when nothing is -- then the evaluation as it stands is drawn.\n *\n * Also null when the figure cannot take the value in hand (a size of 0 on the way to typing 0.5, a glow the material\n * refuses): the canvas keeps the figure as it stands until the release, and the release reports why through the same\n * refusal every committed edit gets. A preview never invents a figure the commit would not make.\n */\nexport function livePartsOf(asset: FigureAsset, live: LiveValues): FigureRenderPart[] | null {\n if (!isLive(live)) return null\n try {\n let next = live.edit ? editFigurePart(asset, live.edit) : asset\n if (live.action) next = applyFigureAuthoring(next, live.action)\n /* Read against the figure the edit makes: a command can take an input away (one wall becomes one per side). */\n const { design, state } = liveOverridesOf(next, live.values)\n /*\n * An input the edit or the command in hand wrote takes its value: the values as they stand hold its old one (a\n * point's coordinate before it was moved, a corner's radius before it was typed again) and would put it back.\n */\n for (const id of Object.keys(design)) if (next.designInputs[id] !== asset.designInputs[id]) delete design[id]\n if (Object.keys(design).length) next = { ...next, designInputs: { ...next.designInputs, ...design } }\n return drawableFigureParts(compileFigureAsset(next).evaluate(state).geometry)\n } catch {\n return null\n }\n}\n\n/**\n * How much one pixel of a label drag moves a value: a degree for an angle, a hundredth for a ratio, and for a length a\n * step that suits its size -- a millimetre under 100, ten over 1,000 -- so a 12 m wall and a 5 mm pin both scrub at a\n * usable pace. Shift makes it ten times finer.\n */\nexport function scrubStep(unit: string, value: number, fine = false): number {\n const base =\n unit === 'deg' ? 1 : unit === 'ratio' ? 0.01 : Math.max(1, Math.pow(10, Math.floor(Math.log10(Math.max(1, Math.abs(value)))) - 2))\n return fine ? base / 10 : base\n}\n\n/** A scrubbed value on the step's grid, so a drag does not leave 1203.0000000002 behind. */\nexport function scrubbed(start: number, pixels: number, step: number): number {\n const value = start + pixels * step\n const digits = Math.max(0, -Math.floor(Math.log10(step)))\n return Number(value.toFixed(digits))\n}\n\n/**\n * The bodies to draw while a value is in hand, over every input as the session holds it now (`current`): the pose the\n * author has already set with the other controls, and the design values. Drawing over the asset's saved defaults put\n * every other joint back to its saved angle while one slider moved (user, 2026-09-24: the arm jumped on release).\n */\nexport function liveOverSession(asset: FigureAsset, current: Record<string, number>, live: LiveValues): FigureRenderPart[] | null {\n if (!isLive(live)) return null\n return livePartsOf(asset, { ...live, values: { ...current, ...live.values } })\n}\n\n"]}
@@ -1,7 +1,14 @@
1
1
  import type { FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model';
2
+ type Axis = 'x' | 'y' | 'z';
3
+ type End = 'start' | 'end';
4
+ /** The face each side of a section is, and each end of a push: a push's start is its low side. */
5
+ export declare const SIDE_FACE: Record<string, string>;
6
+ export declare const END_FACE: (axis: Axis, end: End) => string;
2
7
  /** The one value every entry holds, or undefined when they differ (or there are none). */
3
8
  export declare const sameOf: (values: readonly (number | undefined)[]) => number | undefined;
4
9
  export declare const mm: (value: number) => number;
10
+ /** The four sides of a push's section, in the kernel's order: the first plane axis low and high, then the second. */
11
+ export declare const sidesOf: (axis: Axis) => string[];
5
12
  /** The wall a new inside starts with: a tenth of the section's narrower side, at least 1 mm. */
6
13
  export declare function defaultWallOf(shape: FigureEvaluatedShape): number;
7
14
  /**
@@ -9,3 +16,29 @@ export declare function defaultWallOf(shape: FigureEvaluatedShape): number;
9
16
  * wall all round, a closed start as thick as the wall and an open end -- a tray standing on its floor.
10
17
  */
11
18
  export declare function cavityRequestOf(shape: FigureEvaluatedShape, patch?: Partial<FigureCavityRequest>): FigureCavityRequest;
19
+ /** The most a rectangle's corners can reach: half its narrower side (the kernel's CORNER_TOO_LARGE says where). */
20
+ export declare function cornerLimitOf(shape: FigureEvaluatedShape): number | undefined;
21
+ /** Whether a corner radius can be taken; if not, the most it can be. */
22
+ export declare function cornerRefusalOf(shape: FigureEvaluatedShape, radius: number): {
23
+ max: number;
24
+ } | null;
25
+ /** One side's wall, the others kept: it must leave room inside, so under the width across it less the opposite wall. */
26
+ export declare function sideWallLimitOf(shape: FigureEvaluatedShape, wall: Record<string, number>, side: string): number;
27
+ /** A wall must leave room inside: under half the narrower width the section has. */
28
+ export declare const wallLimitOf: (shape: FigureEvaluatedShape) => number;
29
+ export type FigureCavityAsk = {
30
+ code: 'wall-too-thick';
31
+ max: number;
32
+ } | {
33
+ code: 'ends-too-thick';
34
+ max: number;
35
+ } | {
36
+ code: 'not-positive';
37
+ };
38
+ /** Whether an inside can be taken, asked of the kernel; if not, what to change and the most it can be. */
39
+ export declare function cavityRefusalOf(shape: FigureEvaluatedShape, request: FigureCavityRequest): FigureCavityAsk | null;
40
+ /** What the inside makes of the part (§3.3): open at both ends a pipe, at one a container, at neither a closed space. */
41
+ export declare function cavityResultOf(cavity: NonNullable<FigureEvaluatedShape['cavity']>): 'pipe' | 'container' | 'sealed';
42
+ /** How deep the inside is along the push: the length less the closed ends. */
43
+ export declare function cavityDepthOf(shape: FigureEvaluatedShape): number;
44
+ export {};