@things-factory/figure-ui 10.1.104 → 10.1.106
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/canvas.ts +6 -13
- package/client/modeller/editor.ts +15 -4
- package/client/modeller/fields.ts +7 -1
- package/client/modeller/figure-cavity-drawing.ts +80 -18
- package/client/modeller/figure-links.ts +49 -35
- package/client/modeller/figure-material-panel.ts +0 -1
- package/client/modeller/figure-parts.ts +3 -3
- package/client/modeller/figure-section-editor.ts +106 -11
- package/client/modeller/figure-section-panel.ts +15 -103
- package/client/modeller/section-drawing.ts +27 -4
- package/dist-client/modeller/canvas.d.ts +0 -2
- package/dist-client/modeller/canvas.js +6 -17
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor.js +17 -4
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fields.js +7 -1
- package/dist-client/modeller/fields.js.map +1 -1
- package/dist-client/modeller/figure-cavity-drawing.d.ts +6 -0
- package/dist-client/modeller/figure-cavity-drawing.js +81 -16
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +2 -2
- package/dist-client/modeller/figure-links.js +49 -34
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.js +0 -1
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +2 -1
- package/dist-client/modeller/figure-parts.js +2 -2
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +13 -0
- package/dist-client/modeller/figure-section-editor.js +109 -9
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.d.ts +0 -1
- package/dist-client/modeller/figure-section-panel.js +14 -73
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +12 -1
- package/dist-client/modeller/section-drawing.js +23 -3
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/cavity-drawing.test.ts +10 -1
- package/test/command-preview.test.ts +11 -4
- package/test/first-minute.test.ts +6 -2
- package/test/layout.test.ts +11 -0
- package/test/section-editor.test.ts +32 -3
- package/test/section-panel.test.ts +5 -3
- package/translations/en.json +2 -10
- package/translations/ja.json +2 -10
- package/translations/ko.json +2 -10
- package/translations/ms.json +2 -10
- package/translations/zh.json +2 -10
|
@@ -6,8 +6,9 @@ import { i18next, localize } from '@operato/i18n'
|
|
|
6
6
|
import { figureCavitySectionOf, figureSectionSidesOf } from '@hatiolab/figure-model'
|
|
7
7
|
import type { FigureAuthoringAction, FigureEvaluatedShape, FigureSection, FigureSectionMeasure, FigureSectionPoint } from '@hatiolab/figure-model'
|
|
8
8
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
9
|
-
import { apexOver, drawnOf, insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf, type SectionWorking } from './section-drawing.js'
|
|
10
|
-
import { END_FACE } from './section-controls.js'
|
|
9
|
+
import { apexOver, drawnOf, insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf, workingShapeOf, type SectionWorking } from './section-drawing.js'
|
|
10
|
+
import { cavityDepthOf, cavityRequestOf, cavityResultOf, END_FACE, sameOf, sidesOf } from './section-controls.js'
|
|
11
|
+
import './figure-cavity-drawing.js'
|
|
11
12
|
import { openPopup } from '@operato/layout'
|
|
12
13
|
import './figure-section-close-choice.js'
|
|
13
14
|
import type { SectionCloseChoice } from './figure-section-close-choice.js'
|
|
@@ -17,6 +18,10 @@ type Axis = 'x' | 'y' | 'z'
|
|
|
17
18
|
/** How the section is laid on the screen, by the axis it is pushed along: across to the right, then down. */
|
|
18
19
|
const SCREEN: Record<Axis, [Axis, Axis]> = { y: ['x', 'z'], x: ['z', 'y'], z: ['x', 'y'] }
|
|
19
20
|
|
|
21
|
+
/** The inside said so two can be compared: its walls by side, its ends. */
|
|
22
|
+
const cavityKey = (w: SectionWorking) =>
|
|
23
|
+
w.cavity ? [Object.entries(w.cavity.wall).sort(), w.cavity.start, w.cavity.end] : null
|
|
24
|
+
|
|
20
25
|
/** The editor's width and room for the solid beside it: below this the two take turns. */
|
|
21
26
|
export const NARROW_BELOW = 900
|
|
22
27
|
|
|
@@ -35,6 +40,10 @@ export const NARROW_BELOW = 900
|
|
|
35
40
|
* apex is dragged or typed like a point, and is not offered while something is fastened to the end it takes away (the
|
|
36
41
|
* kernel's own refusal, `pointBlocked`).
|
|
37
42
|
*
|
|
43
|
+
* A rectangle's or ellipse's inside is made, changed or taken away here too, by the same drawing the section panel has
|
|
44
|
+
* (figure-cavity-drawing), drawn and judged on the widths being typed: walls that fit only a wider section are taken
|
|
45
|
+
* with it, in the same command (figure-model `set-section`'s `cavity`).
|
|
46
|
+
*
|
|
38
47
|
* What the kernel would refuse is marked on the outline and said in the list; apply is held back, and the solid shows
|
|
39
48
|
* the last valid state, which the caption names. Apply sends one `set-section` command (one step of the history); cancel
|
|
40
49
|
* or Esc sends nothing and lets the preview go -- after asking, when something was changed: apply it, let it go, or keep
|
|
@@ -192,6 +201,26 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
192
201
|
min-width: 0;
|
|
193
202
|
font: inherit;
|
|
194
203
|
}
|
|
204
|
+
div[cavity] {
|
|
205
|
+
display: grid;
|
|
206
|
+
gap: 6px;
|
|
207
|
+
margin-top: 10px;
|
|
208
|
+
padding-top: 8px;
|
|
209
|
+
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
210
|
+
}
|
|
211
|
+
/* The panel's width: wider, the drawing pushed the outline out of view. */
|
|
212
|
+
div[cavity] figure-cavity-drawing {
|
|
213
|
+
max-width: 340px;
|
|
214
|
+
}
|
|
215
|
+
label[toggle] {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 6px;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
}
|
|
221
|
+
label[toggle] small[result] {
|
|
222
|
+
margin-left: auto;
|
|
223
|
+
}
|
|
195
224
|
ul[issues] {
|
|
196
225
|
margin: 8px 0 0;
|
|
197
226
|
padding: 0 0 0 16px;
|
|
@@ -256,7 +285,9 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
256
285
|
if (!this.narrow) this.show = 'outline'
|
|
257
286
|
})
|
|
258
287
|
|
|
259
|
-
@state() private working: SectionWorking = { points: [], across: {}, length: 0, apex: null }
|
|
288
|
+
@state() private working: SectionWorking = { points: [], across: {}, length: 0, apex: null, cavity: null }
|
|
289
|
+
/** Walls one per side in the inside's drawing; on when they open differing. */
|
|
290
|
+
@state() private perSide = false
|
|
260
291
|
@state() private selected: string | null = null
|
|
261
292
|
@state() private note = ''
|
|
262
293
|
private opened!: SectionWorking
|
|
@@ -274,6 +305,10 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
274
305
|
private get kind() {
|
|
275
306
|
return this.body && drawnOf(this.body.shape)?.section.kind
|
|
276
307
|
}
|
|
308
|
+
/** A pushed rectangle or ellipse can have an inside; a drawn section's is a later stage. */
|
|
309
|
+
private get hollowable() {
|
|
310
|
+
return !!this.body?.shape.extrusion && (this.kind === 'rectangle' || this.kind === 'ellipse')
|
|
311
|
+
}
|
|
277
312
|
|
|
278
313
|
connectedCallback() {
|
|
279
314
|
super.connectedCallback()
|
|
@@ -282,6 +317,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
282
317
|
this.lastValid = this.working
|
|
283
318
|
this.openedAt = this.revision
|
|
284
319
|
this.extent = this.extentOf(this.working)
|
|
320
|
+
this.perSide = !!this.working.cavity && sameOf(Object.values(this.working.cavity.wall)) === undefined
|
|
285
321
|
window.addEventListener('keydown', this.keys, true)
|
|
286
322
|
if (this.parentElement) this.room.observe(this.parentElement)
|
|
287
323
|
}
|
|
@@ -310,6 +346,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
310
346
|
*/
|
|
311
347
|
private keys = (e: KeyboardEvent) => {
|
|
312
348
|
const inField = (e.composedPath()[0] as HTMLElement)?.tagName === 'INPUT'
|
|
349
|
+
/* A number being typed in the inside's drawing: Esc there puts the number back, not the editor away. */
|
|
350
|
+
if (inField && e.composedPath().some(el => (el as HTMLElement).tagName === 'FIGURE-CAVITY-DRAWING')) return
|
|
313
351
|
/* While the close is being asked, the question has the keys: Esc there is 계속 편집. */
|
|
314
352
|
if (this.asking) return
|
|
315
353
|
const take = () => (e.preventDefault(), e.stopImmediatePropagation())
|
|
@@ -351,10 +389,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
351
389
|
}
|
|
352
390
|
|
|
353
391
|
/** The solid behind shows the working state, or the last valid one while the working one is refused. */
|
|
354
|
-
private preview() {
|
|
355
|
-
this.dispatchEvent(
|
|
356
|
-
new CustomEvent('command-live', { detail: { action: this.action(this.lastValid) }, bubbles: true, composed: true })
|
|
357
|
-
)
|
|
392
|
+
private preview(w = this.lastValid) {
|
|
393
|
+
this.dispatchEvent(new CustomEvent('command-live', { detail: { action: this.action(w) }, bubbles: true, composed: true }))
|
|
358
394
|
}
|
|
359
395
|
|
|
360
396
|
/** One command for everything changed since the editor opened; a measure not changed is not sent (it may follow another). */
|
|
@@ -363,13 +399,15 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
363
399
|
const changed = (a: number | undefined, b: number | undefined) => Math.abs((a ?? 0) - (b ?? 0)) > 1e-9
|
|
364
400
|
const across = Object.fromEntries(Object.entries(w.across).filter(([k, v]) => changed(v, o.across[k as Axis])))
|
|
365
401
|
const apexMoved = !w.apex !== !o.apex || (!!w.apex && Object.entries(w.apex).some(([k, v]) => changed(v, o.apex![k as Axis])))
|
|
402
|
+
const cavityMoved = JSON.stringify(cavityKey(w)) !== JSON.stringify(cavityKey(o))
|
|
366
403
|
return {
|
|
367
404
|
kind: 'set-section',
|
|
368
405
|
part: this.body!.id,
|
|
369
406
|
...(this.kind === 'polygon' ? { points: w.points.map(p => ({ ...p })) } : {}),
|
|
370
407
|
...(Object.keys(across).length ? { across } : {}),
|
|
371
408
|
...(changed(w.length, o.length) ? { length: w.length } : {}),
|
|
372
|
-
...(apexMoved ? { apex: w.apex ? { ...w.apex } : null } : {})
|
|
409
|
+
...(apexMoved ? { apex: w.apex ? { ...w.apex } : null } : {}),
|
|
410
|
+
...(cavityMoved ? { cavity: w.cavity ? { wall: { ...w.cavity.wall }, start: w.cavity.start, end: w.cavity.end } : null } : {})
|
|
373
411
|
}
|
|
374
412
|
}
|
|
375
413
|
|
|
@@ -403,7 +441,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
403
441
|
w.points.map(p => [p.id, p.x ?? 0, p.y ?? 0, p.z ?? 0]),
|
|
404
442
|
Object.entries(w.across).sort(),
|
|
405
443
|
w.length,
|
|
406
|
-
w.apex ? Object.entries(w.apex).sort() : null
|
|
444
|
+
w.apex ? Object.entries(w.apex).sort() : null,
|
|
445
|
+
cavityKey(w)
|
|
407
446
|
])
|
|
408
447
|
return !!this.opened && key(this.working) !== key(this.opened)
|
|
409
448
|
}
|
|
@@ -529,8 +568,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
529
568
|
const grid = Math.max(10, Math.pow(10, Math.floor(Math.log10(e))) / 2)
|
|
530
569
|
const lines: number[] = []
|
|
531
570
|
for (let g = -Math.floor(e / grid) * grid; g <= e; g += grid) lines.push(g)
|
|
532
|
-
const
|
|
533
|
-
const inside = cavity && !invalid ? figureCavitySectionOf(section, this.axis, cavity.wall) : undefined
|
|
571
|
+
const inside = w.cavity && !invalid ? figureCavitySectionOf(section, this.axis, w.cavity.wall) : undefined
|
|
534
572
|
const sized = (axis: Axis) => this.measures?.measures[axis] === 'editable'
|
|
535
573
|
return html`<div caption ?last-valid=${invalid}>
|
|
536
574
|
${invalid ? i18next.t('figure.text.section-editor-last-valid') : i18next.t('figure.text.section-editor-units', { h: h.toUpperCase(), v: v.toUpperCase() })}
|
|
@@ -710,6 +748,62 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
710
748
|
: nothing}`
|
|
711
749
|
}
|
|
712
750
|
|
|
751
|
+
/** The inside, as the section panel shows it, on the working section: on or off, what it makes, its drawing. */
|
|
752
|
+
private cavityBlock(invalid: boolean) {
|
|
753
|
+
const w = this.working
|
|
754
|
+
const shape = workingShapeOf(this.body!.shape, w)
|
|
755
|
+
return html`<div cavity>
|
|
756
|
+
<label toggle>
|
|
757
|
+
<input
|
|
758
|
+
type="checkbox"
|
|
759
|
+
.checked=${live(!!w.cavity)}
|
|
760
|
+
@change=${(ev: Event) => {
|
|
761
|
+
const on = (ev.target as HTMLInputElement).checked
|
|
762
|
+
const ask = cavityRequestOf(shape, {})
|
|
763
|
+
const wall = typeof ask.wall === 'number' ? Object.fromEntries(sidesOf(this.axis).map(side => [side, ask.wall as number])) : (ask.wall as Record<string, number>)
|
|
764
|
+
this.change({ ...w, cavity: on ? { wall, start: ask.start, end: ask.end } : null })
|
|
765
|
+
}}
|
|
766
|
+
/>
|
|
767
|
+
${i18next.t('figure.label.section-cavity')}
|
|
768
|
+
${w.cavity && !invalid
|
|
769
|
+
? html`<small result>
|
|
770
|
+
${i18next.t('figure.text.section-depth', {
|
|
771
|
+
result: i18next.t(`figure.label.section-result-${cavityResultOf(shape.cavity!)}`),
|
|
772
|
+
depth: String(cavityDepthOf(shape))
|
|
773
|
+
})}
|
|
774
|
+
</small>`
|
|
775
|
+
: nothing}
|
|
776
|
+
</label>
|
|
777
|
+
${w.cavity
|
|
778
|
+
? html`<figure-cavity-drawing
|
|
779
|
+
.body=${{ id: this.body!.id, shape }}
|
|
780
|
+
.perSide=${this.perSide}
|
|
781
|
+
@command-live=${(ev: CustomEvent) => {
|
|
782
|
+
ev.stopPropagation()
|
|
783
|
+
const c = this.cavityOf(ev.detail.action)
|
|
784
|
+
this.preview(c && this.valid({ ...this.working, cavity: c }) ? { ...this.working, cavity: c } : this.lastValid)
|
|
785
|
+
}}
|
|
786
|
+
@command=${(ev: CustomEvent) => {
|
|
787
|
+
ev.stopPropagation()
|
|
788
|
+
const c = this.cavityOf(ev.detail.action)
|
|
789
|
+
if (c) this.change({ ...this.working, cavity: c })
|
|
790
|
+
}}
|
|
791
|
+
></figure-cavity-drawing>
|
|
792
|
+
<label toggle>
|
|
793
|
+
<input type="checkbox" .checked=${live(this.perSide)} @change=${(ev: Event) => (this.perSide = (ev.target as HTMLInputElement).checked)} />
|
|
794
|
+
${i18next.t('figure.label.section-wall-per-side')}
|
|
795
|
+
</label>`
|
|
796
|
+
: nothing}
|
|
797
|
+
</div>`
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** The inside a drawing's set-cavity asks for, every wall written out; the drawing's commands stay in the editor. */
|
|
801
|
+
private cavityOf(action: FigureAuthoringAction | null): SectionWorking['cavity'] | undefined {
|
|
802
|
+
if (action?.kind !== 'set-cavity' || !action.cavity) return undefined
|
|
803
|
+
const { wall, start, end } = action.cavity
|
|
804
|
+
return { wall: typeof wall === 'number' ? Object.fromEntries(sidesOf(this.axis).map(side => [side, wall])) : ({ ...wall } as Record<string, number>), start, end }
|
|
805
|
+
}
|
|
806
|
+
|
|
713
807
|
private list(issues: ReturnType<typeof workingIssuesOf>['issues'], words: ReturnType<typeof workingIssuesOf>['words']) {
|
|
714
808
|
const [h, v] = SCREEN[this.axis]
|
|
715
809
|
const marked = markedBy(issues)
|
|
@@ -773,6 +867,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
773
867
|
)}
|
|
774
868
|
</ol>`
|
|
775
869
|
: nothing}
|
|
870
|
+
${this.hollowable ? this.cavityBlock(words.length > 0) : nothing}
|
|
776
871
|
${this.note ? html`<small role="status">${this.note}</small>` : nothing}
|
|
777
872
|
${words.length
|
|
778
873
|
? html`<ul issues role="alert">
|
|
@@ -4,30 +4,12 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
|
4
4
|
import { live } from 'lit/directives/live.js'
|
|
5
5
|
|
|
6
6
|
import { i18next, localize } from '@operato/i18n'
|
|
7
|
-
import type { FigureAuthoringAction,
|
|
7
|
+
import type { FigureAuthoringAction, FigureEvaluatedShape } from '@hatiolab/figure-model'
|
|
8
8
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
9
9
|
import { drawnOf } from './section-drawing.js'
|
|
10
10
|
import './figure-cavity-drawing.js'
|
|
11
|
-
import {
|
|
12
|
-
END_FACE,
|
|
13
|
-
SIDE_FACE,
|
|
14
|
-
cavityDepthOf,
|
|
15
|
-
cavityRefusalOf,
|
|
16
|
-
cavityRequestOf,
|
|
17
|
-
cavityResultOf,
|
|
18
|
-
cornerLimitOf,
|
|
19
|
-
cornerRefusalOf,
|
|
20
|
-
defaultWallOf,
|
|
21
|
-
mm,
|
|
22
|
-
sameOf,
|
|
23
|
-
sidesOf,
|
|
24
|
-
sideWallLimitOf,
|
|
25
|
-
wallLimitOf,
|
|
26
|
-
type FigureCavityAsk
|
|
27
|
-
} from './section-controls.js'
|
|
11
|
+
import { cavityDepthOf, cavityRequestOf, cavityResultOf, cornerLimitOf, cornerRefusalOf, mm, sameOf } from './section-controls.js'
|
|
28
12
|
|
|
29
|
-
type Axis = 'x' | 'y' | 'z'
|
|
30
|
-
type End = 'start' | 'end'
|
|
31
13
|
type Body = { id: string; shape: FigureEvaluatedShape }
|
|
32
14
|
|
|
33
15
|
/**
|
|
@@ -114,7 +96,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
|
|
|
114
96
|
</div>`
|
|
115
97
|
: nothing}
|
|
116
98
|
${kind === 'rectangle' ? this.renderCorner(body) : nothing}
|
|
117
|
-
${/* No inside for a drawn section: the control is not there, rather than a line saying so. */ kind === 'rectangle' || kind === 'ellipse' ? this.renderCavity(body
|
|
99
|
+
${/* No inside for a drawn section: the control is not there, rather than a line saying so. */ kind === 'rectangle' || kind === 'ellipse' ? this.renderCavity(body) : nothing}
|
|
118
100
|
</section>`
|
|
119
101
|
}
|
|
120
102
|
|
|
@@ -142,29 +124,13 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
|
|
|
142
124
|
</div>`
|
|
143
125
|
}
|
|
144
126
|
|
|
145
|
-
|
|
127
|
+
/*
|
|
128
|
+
* The inside: made or taken away here, and drawn -- walls dragged or their number pressed and typed, ends pressed
|
|
129
|
+
* open or closed (figure-cavity-drawing). The number fields under the drawing went (user 2026-10-01): they said the
|
|
130
|
+
* drawing again.
|
|
131
|
+
*/
|
|
132
|
+
private renderCavity(body: Body) {
|
|
146
133
|
const cavity = body.shape.cavity
|
|
147
|
-
const request = (patch: Partial<FigureCavityRequest>) => cavityRequestOf(body.shape, patch)
|
|
148
|
-
const said = (ask: FigureCavityAsk | null) =>
|
|
149
|
-
!ask
|
|
150
|
-
? ''
|
|
151
|
-
: ask.code === 'not-positive'
|
|
152
|
-
? i18next.t('figure.text.section-not-positive')
|
|
153
|
-
: i18next.t(`figure.text.section-${ask.code === 'wall-too-thick' ? 'wall' : 'ends'}-over`, { max: String(ask.max) })
|
|
154
|
-
const cavityField = (key: string, now: number | undefined, patch: (value: number) => Partial<FigureCavityRequest>) =>
|
|
155
|
-
this.number(key, now, {
|
|
156
|
-
min: 0,
|
|
157
|
-
ask: value => {
|
|
158
|
-
if (!(value > 0)) return i18next.t('figure.text.section-not-positive')
|
|
159
|
-
const ask = cavityRefusalOf(body.shape, request(patch(value)))
|
|
160
|
-
/* One side too thick: the most it can be is the width across less the wall opposite, not the even limit. */
|
|
161
|
-
const side = key.startsWith('wall.') ? key.slice(5) : undefined
|
|
162
|
-
if (ask?.code === 'wall-too-thick' && side && cavity)
|
|
163
|
-
return said({ code: 'wall-too-thick', max: mm(sideWallLimitOf(body.shape, cavity.wall, side)) })
|
|
164
|
-
return said(ask)
|
|
165
|
-
},
|
|
166
|
-
action: value => ({ kind: 'set-cavity', part: body.id, cavity: request(patch(value)) })
|
|
167
|
-
})
|
|
168
134
|
return html`<div field>
|
|
169
135
|
<label toggle>
|
|
170
136
|
<input
|
|
@@ -174,7 +140,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
|
|
|
174
140
|
@change=${(ev: Event) => {
|
|
175
141
|
const on = (ev.target as HTMLInputElement).checked
|
|
176
142
|
;(ev.target as HTMLInputElement).checked = !!cavity
|
|
177
|
-
this.command({ kind: 'set-cavity', part: body.id, cavity: on ?
|
|
143
|
+
this.command({ kind: 'set-cavity', part: body.id, cavity: on ? cavityRequestOf(body.shape, {}) : null })
|
|
178
144
|
}}
|
|
179
145
|
/>
|
|
180
146
|
${i18next.t('figure.label.section-cavity')}
|
|
@@ -188,69 +154,15 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
|
|
|
188
154
|
</small>`
|
|
189
155
|
: nothing}
|
|
190
156
|
</div>
|
|
191
|
-
${cavity ? html`<figure-cavity-drawing .body=${body} .perSide=${this.perSide} ?locked=${this.locked}></figure-cavity-drawing>` : nothing}
|
|
192
157
|
${cavity
|
|
193
|
-
? html`<
|
|
194
|
-
|
|
195
|
-
<
|
|
196
|
-
${
|
|
197
|
-
|
|
198
|
-
${[...sidesOf(axis)].sort((a, b) => a[0]!.localeCompare(b[0]!) || (a[1] === '-' ? -1 : 1)).map(
|
|
199
|
-
side => html`<label>
|
|
200
|
-
<span>${i18next.t('figure.label.face-' + SIDE_FACE[side])}</span>
|
|
201
|
-
${cavityField(`wall.${side}`, cavity.wall[side], value => ({ wall: { ...cavity.wall, [side]: value } }))}
|
|
202
|
-
</label>`
|
|
203
|
-
)}
|
|
204
|
-
</div>`
|
|
205
|
-
: cavityField('wall', sameOf(Object.values(cavity.wall)), value => ({ wall: value }))}
|
|
206
|
-
${this.perSide || this.refusals.wall
|
|
207
|
-
? nothing
|
|
208
|
-
: html`<small>${i18next.t('figure.text.section-wall-at-most', { max: String(mm(wallLimitOf(body.shape))) })}</small>`}
|
|
209
|
-
<label toggle>
|
|
210
|
-
<input
|
|
211
|
-
type="checkbox"
|
|
212
|
-
.checked=${live(this.perSide)}
|
|
213
|
-
?disabled=${this.locked}
|
|
214
|
-
@change=${(ev: Event) => (this.perSide = (ev.target as HTMLInputElement).checked)}
|
|
215
|
-
/>
|
|
216
|
-
${i18next.t('figure.label.section-wall-per-side')}
|
|
217
|
-
</label>
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
220
|
-
${(['start', 'end'] as const).map(end => this.renderEnd(body, axis, end, cavity[end], request, cavityField))}`
|
|
158
|
+
? html`<figure-cavity-drawing .body=${body} .perSide=${this.perSide} ?locked=${this.locked}></figure-cavity-drawing>
|
|
159
|
+
<label toggle>
|
|
160
|
+
<input type="checkbox" .checked=${live(this.perSide)} ?disabled=${this.locked} @change=${(ev: Event) => (this.perSide = (ev.target as HTMLInputElement).checked)} />
|
|
161
|
+
${i18next.t('figure.label.section-wall-per-side')}
|
|
162
|
+
</label>`
|
|
221
163
|
: nothing}`
|
|
222
164
|
}
|
|
223
165
|
|
|
224
|
-
private renderEnd(
|
|
225
|
-
body: Body,
|
|
226
|
-
axis: Axis,
|
|
227
|
-
end: End,
|
|
228
|
-
now: 'open' | { closed: number },
|
|
229
|
-
request: (patch: Partial<FigureCavityRequest>) => FigureCavityRequest,
|
|
230
|
-
cavityField: (key: string, now: number | undefined, patch: (value: number) => Partial<FigureCavityRequest>) => unknown
|
|
231
|
-
) {
|
|
232
|
-
const closed = now !== 'open'
|
|
233
|
-
return html`<div form>
|
|
234
|
-
<label>${i18next.t('figure.label.face-' + END_FACE(axis, end))}</label>
|
|
235
|
-
<div row>
|
|
236
|
-
<select
|
|
237
|
-
.value=${live(closed ? 'closed' : 'open')}
|
|
238
|
-
?disabled=${this.locked}
|
|
239
|
-
@change=${(ev: Event) => {
|
|
240
|
-
const select = ev.target as HTMLSelectElement
|
|
241
|
-
const want = select.value
|
|
242
|
-
select.value = closed ? 'closed' : 'open'
|
|
243
|
-
this.command({ kind: 'set-cavity', part: body.id, cavity: request({ [end]: want === 'open' ? 'open' : { closed: defaultWallOf(body.shape) } }) })
|
|
244
|
-
}}
|
|
245
|
-
>
|
|
246
|
-
<option value="closed">${i18next.t('figure.label.section-end-closed')}</option>
|
|
247
|
-
<option value="open">${i18next.t('figure.label.section-end-open')}</option>
|
|
248
|
-
</select>
|
|
249
|
-
${closed ? cavityField(`closed.${end}`, now.closed, value => ({ [end]: { closed: value } })) : html`<span></span>`}
|
|
250
|
-
</div>
|
|
251
|
-
</div>`
|
|
252
|
-
}
|
|
253
|
-
|
|
254
166
|
/**
|
|
255
167
|
* A millimetre field. Typing draws the value (or, if the kernel would refuse it, says why under the field and draws the
|
|
256
168
|
* figure as it stands); Enter or leaving sends it once; Esc puts the value back.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FIGURE_SECTION_PLANE, FIGURE_SECTION_POINT_LIMIT, checkFigureSection, figureExtrusionFeaturesOf, figureSectionSidesOf } from '@hatiolab/figure-model'
|
|
2
|
-
import type { FigureEvaluatedShape, FigureSection, FigureSectionIssue, FigureSectionPoint } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureCavityRequest, FigureEvaluatedShape, FigureSection, FigureSectionIssue, FigureSectionPoint } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* What the section editor does to a drawn section's points, and how it says what is wrong with them (ADR-0100 §3.2).
|
|
@@ -106,6 +106,8 @@ export interface SectionWorking {
|
|
|
106
106
|
length: number
|
|
107
107
|
/** A drawn section's end drawn to one point (a pyramid), across the plane axes; null for a push. */
|
|
108
108
|
apex: Partial<Record<Axis, number>> | null
|
|
109
|
+
/** A pushed rectangle's or ellipse's inside, every side's wall written out; null for none or a drawn section. */
|
|
110
|
+
cavity: { wall: Record<string, number>; start: FigureCavityRequest['start']; end: FigureCavityRequest['end'] } | null
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
/** A pushed part's section, axis and ends -- or a pyramid's, whose end is its apex (figure-model `loft@1`). */
|
|
@@ -135,7 +137,28 @@ export function workingOf(shape: FigureEvaluatedShape): SectionWorking {
|
|
|
135
137
|
: s.kind === 'ellipse'
|
|
136
138
|
? { [u]: 2 * s.radius[u]!, [v]: 2 * s.radius[v]! }
|
|
137
139
|
: {}
|
|
138
|
-
|
|
140
|
+
const c = shape.extrusion && shape.cavity
|
|
141
|
+
return {
|
|
142
|
+
points: s.kind === 'polygon' ? s.points.map(p => ({ ...p })) : [],
|
|
143
|
+
across,
|
|
144
|
+
length: e.end - e.start,
|
|
145
|
+
apex: e.apex ? { ...e.apex } : null,
|
|
146
|
+
cavity: c ? { wall: { ...c.wall }, start: c.start, end: c.end } : null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The part as the working state would make it: its section, its length from the same start, its inside -- what the
|
|
152
|
+
* inside's drawing reads, so its walls are judged against the widths being typed and not the ones the editor opened with.
|
|
153
|
+
*/
|
|
154
|
+
export function workingShapeOf(shape: FigureEvaluatedShape, w: SectionWorking): FigureEvaluatedShape {
|
|
155
|
+
const e = shape.extrusion!
|
|
156
|
+
const { cavity: _, ...rest } = shape
|
|
157
|
+
return {
|
|
158
|
+
...rest,
|
|
159
|
+
extrusion: { ...e, section: sectionOf(shape, w), end: e.start + w.length },
|
|
160
|
+
...(w.cavity ? { cavity: { id: shape.cavity?.id ?? 'inside', wall: { ...w.cavity.wall }, start: w.cavity.start, end: w.cavity.end } } : {})
|
|
161
|
+
} as FigureEvaluatedShape
|
|
139
162
|
}
|
|
140
163
|
|
|
141
164
|
/** The section the working state makes, of the part's own kind: its corners kept, its points or widths the working ones. */
|
|
@@ -159,8 +182,8 @@ export function workingIssuesOf(
|
|
|
159
182
|
const words = issues.map(i => issueWords(i, w.points, e.axis))
|
|
160
183
|
if (!(Number.isFinite(w.length) && w.length > 0)) words.push({ key: 'figure.text.section-issue-length', params: {} })
|
|
161
184
|
if (w.apex && !Object.values(w.apex).every(n => Number.isFinite(n))) words.push({ key: 'figure.text.section-issue-number', params: {} })
|
|
162
|
-
if (!issues.length && words.length === 0 &&
|
|
163
|
-
const made = figureExtrusionFeaturesOf({ axis: e.axis, section, start: e.start, end: e.start + w.length }, [
|
|
185
|
+
if (!issues.length && words.length === 0 && w.cavity) {
|
|
186
|
+
const made = figureExtrusionFeaturesOf({ axis: e.axis, section, start: e.start, end: e.start + w.length }, [{ id: 'inside', ...w.cavity }])
|
|
164
187
|
if ('refused' in made) {
|
|
165
188
|
const r = made.refused
|
|
166
189
|
words.push({ key: r.code === 'CAVITY_NO_LENGTH' ? 'figure.text.section-issue-cavity-length' : 'figure.text.section-issue-cavity-walls', params: {} })
|
|
@@ -21,8 +21,6 @@ export declare class FigureCanvas extends LitElement {
|
|
|
21
21
|
* would reach the editor as an edit.
|
|
22
22
|
*/
|
|
23
23
|
viewOnly: boolean;
|
|
24
|
-
/** The editor shows a new figure's start card over the canvas: the canvas's own one-line hint gives way to it. */
|
|
25
|
-
startShown: boolean;
|
|
26
24
|
/** Where the selected part turns and how far (`joint-fan`): shown, never picked, never saved. */
|
|
27
25
|
fan: FigureRenderPart | null;
|
|
28
26
|
/**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
|
-
import { LitElement, html, css,
|
|
2
|
+
import { LitElement, html, css, unsafeCSS } from 'lit';
|
|
3
3
|
import { customElement, property, query } from 'lit/decorators.js';
|
|
4
|
-
import { i18next } from '@operato/i18n';
|
|
5
4
|
import { create, Model, niceGridStep } from '@hatiolab/things-scene';
|
|
6
5
|
import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js';
|
|
7
6
|
import { boardFor, changedKeys, componentOf, gizmoResultOf } from './stage-model.js';
|
|
@@ -31,8 +30,6 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
31
30
|
* would reach the editor as an edit.
|
|
32
31
|
*/
|
|
33
32
|
this.viewOnly = false;
|
|
34
|
-
/** The editor shows a new figure's start card over the canvas: the canvas's own one-line hint gives way to it. */
|
|
35
|
-
this.startShown = false;
|
|
36
33
|
/** Where the selected part turns and how far (`joint-fan`): shown, never picked, never saved. */
|
|
37
34
|
this.fan = null;
|
|
38
35
|
/**
|
|
@@ -72,17 +69,13 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
72
69
|
width: 100%;
|
|
73
70
|
height: 100%;
|
|
74
71
|
}
|
|
75
|
-
p {
|
|
76
|
-
position: absolute;
|
|
77
|
-
inset: 45% 1rem auto;
|
|
78
|
-
margin: 0;
|
|
79
|
-
text-align: center;
|
|
80
|
-
pointer-events: none;
|
|
81
|
-
color: #f4f4f4;
|
|
82
|
-
}
|
|
83
72
|
`; }
|
|
84
73
|
render() {
|
|
85
|
-
|
|
74
|
+
/*
|
|
75
|
+
* No line of its own over an empty stage: a new figure's start card says how to begin, and an empty stage otherwise
|
|
76
|
+
* is one loading, or with every part hidden -- where 「도형을 추가해…」 flashed and said the wrong thing (2026-10-02).
|
|
77
|
+
*/
|
|
78
|
+
return html `<div stage></div>`;
|
|
86
79
|
}
|
|
87
80
|
firstUpdated() {
|
|
88
81
|
this.standUp();
|
|
@@ -381,10 +374,6 @@ __decorate([
|
|
|
381
374
|
property({ type: Boolean }),
|
|
382
375
|
__metadata("design:type", Object)
|
|
383
376
|
], FigureCanvas.prototype, "viewOnly", void 0);
|
|
384
|
-
__decorate([
|
|
385
|
-
property({ type: Boolean }),
|
|
386
|
-
__metadata("design:type", Object)
|
|
387
|
-
], FigureCanvas.prototype, "startShown", void 0);
|
|
388
377
|
__decorate([
|
|
389
378
|
property({ attribute: false }),
|
|
390
379
|
__metadata("design:type", Object)
|