@things-factory/figure-ui 10.1.88 → 10.1.89
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 +29 -3
- package/client/modeller/figure-parts.ts +2 -0
- package/client/modeller/figure-section-editor.ts +93 -8
- package/client/modeller/figure-section-panel.ts +3 -1
- package/client/modeller/maturity.ts +8 -1
- package/client/modeller/palette.ts +13 -0
- package/client/modeller/section-drawing.ts +25 -5
- package/client/modeller/starting-shape.ts +5 -3
- package/dist-client/modeller/editor.d.ts +6 -0
- package/dist-client/modeller/editor.js +27 -3
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +2 -0
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +8 -0
- package/dist-client/modeller/figure-section-editor.js +89 -8
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +3 -1
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/maturity.js +8 -1
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/palette.js +13 -0
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +12 -0
- package/dist-client/modeller/section-drawing.js +22 -5
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/starting-shape.d.ts +3 -2
- package/dist-client/modeller/starting-shape.js +4 -0
- package/dist-client/modeller/starting-shape.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/browser/acceptance-page.js +9 -0
- package/test/palette-icons.test.ts +5 -2
- package/test/parts-wiring.test.ts +1 -1
- package/test/section-editor.test.ts +57 -3
- package/translations/en.json +7 -1
- package/translations/ja.json +7 -1
- package/translations/ko.json +7 -1
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
|
@@ -35,6 +35,8 @@ import { ScrollbarStyles } from '@operato/styles'
|
|
|
35
35
|
import { alignChanges, boxOf, canMirror, mirrorChanges, type AlignTool } from './align.js'
|
|
36
36
|
import type { FigurePaletteKind, FigurePartRow } from './figure-parts.js'
|
|
37
37
|
import { paletteRecipe } from './palette.js'
|
|
38
|
+
import { apexOver, drawnOf } from './section-drawing.js'
|
|
39
|
+
import { END_FACE } from './section-controls.js'
|
|
38
40
|
import type { FigureCanvas } from './canvas.js'
|
|
39
41
|
import { nudgeOf, placementOf, type ScaleAnchor } from './stage-model.js'
|
|
40
42
|
|
|
@@ -1785,20 +1787,41 @@ export class FigureEditor extends LitElement {
|
|
|
1785
1787
|
private renderSectionEditor(geometry: readonly { id: string }[]) {
|
|
1786
1788
|
const part = geometry.find(g => ownerOf(g.id) === this.selected) as { id: string; shape: FigureEvaluatedShape } | undefined
|
|
1787
1789
|
/* A door frame's section is its own width, height, jamb and head: its fields edit it, not an outline. */
|
|
1788
|
-
|
|
1790
|
+
const drawn = part?.shape && drawnOf(part.shape)
|
|
1791
|
+
if (!part || !drawn || part.shape.dimensions.jamb !== undefined) return nothing
|
|
1789
1792
|
const asset = this.asset() as FigureAsset
|
|
1790
1793
|
const measures = figureSectionMeasuresOf(asset, this.selected)
|
|
1791
|
-
const kept = figureScaleAnchorOf(asset, this.selected)?.[
|
|
1794
|
+
const kept = figureScaleAnchorOf(asset, this.selected)?.[drawn.axis]
|
|
1792
1795
|
return html`<figure-section-editor
|
|
1793
1796
|
.body=${{ ...part, id: this.selected }}
|
|
1794
1797
|
.measures=${measures}
|
|
1795
1798
|
.kept=${kept}
|
|
1799
|
+
.pointBlocked=${this.pointBlockedOf(drawn)}
|
|
1796
1800
|
.revision=${this.session?.read().revision ?? 0}
|
|
1797
1801
|
@command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
1798
1802
|
@command-live=${(e: CustomEvent) => this.holdAction(e.detail.action)}
|
|
1799
1803
|
@section-editor-close=${() => this.closeSectionEditor()}
|
|
1800
1804
|
></figure-section-editor>`
|
|
1801
1805
|
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Why a drawn push's end cannot be drawn to one point now: the kernel's own refusal of that command on a copy (what is
|
|
1808
|
+
* fastened to the end face), said as the face. Asked once per figure revision and part.
|
|
1809
|
+
*/
|
|
1810
|
+
private pointBlocked?: { key: string; words: string }
|
|
1811
|
+
private pointBlockedOf(drawn: NonNullable<ReturnType<typeof drawnOf>>): string {
|
|
1812
|
+
if (drawn.apex || drawn.section.kind !== 'polygon' || !this.session) return ''
|
|
1813
|
+
const key = `${this.session.read().revision}|${this.selected}`
|
|
1814
|
+
if (this.pointBlocked?.key === key) return this.pointBlocked.words
|
|
1815
|
+
let words = ''
|
|
1816
|
+
try {
|
|
1817
|
+
this.session.previewCommand({ kind: 'set-section', part: this.selected!, apex: apexOver(drawn.section.points, drawn.axis) })
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
if ((error as { code?: string }).code === 'FEATURE_MAPPING_REQUIRED')
|
|
1820
|
+
words = i18next.t('figure.text.section-editor-apex-blocked', { face: i18next.t('figure.label.face-' + END_FACE(drawn.axis, 'end')) })
|
|
1821
|
+
}
|
|
1822
|
+
this.pointBlocked = { key, words }
|
|
1823
|
+
return words
|
|
1824
|
+
}
|
|
1802
1825
|
private renderStart() {
|
|
1803
1826
|
const shapes: FigurePaletteKind[] = ['box', 'cylinder', 'plate']
|
|
1804
1827
|
/* Where it is authored from, chosen before any part (ADR-0097): the figure's own setting, no room made up for it. */
|
|
@@ -2234,9 +2257,12 @@ export class FigureEditor extends LitElement {
|
|
|
2234
2257
|
private renderSingle(name: string, field: FieldLike, unavailable: boolean) {
|
|
2235
2258
|
/* A drawn section's point is called by its number and axis, as the section editor calls it (ADR-0100 §2.1, Q9). */
|
|
2236
2259
|
const point = /^point\.p(\d+)\.([xyz])$/.exec(name)
|
|
2260
|
+
const apex = /^apex\.([xyz])$/.exec(name)
|
|
2237
2261
|
const words = point
|
|
2238
2262
|
? [i18next.t('figure.label.section-editor-coordinate', { point: point[1]!, axis: point[2]!.toUpperCase() })]
|
|
2239
|
-
:
|
|
2263
|
+
: apex
|
|
2264
|
+
? [`${i18next.t('figure.label.section-editor-apex-mark')} ${apex[1]!.toUpperCase()}`]
|
|
2265
|
+
: name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }))
|
|
2240
2266
|
return html`<span ?scrub=${!unavailable} @pointerdown=${(e: PointerEvent) => this.scrub(field as never, e, unavailable)}>${words.join(' · ')} (${field.unit})</span
|
|
2241
2267
|
><input
|
|
2242
2268
|
aria-label=${field.id}
|
|
@@ -46,6 +46,7 @@ const KIND_ICON: Record<string, string> = {
|
|
|
46
46
|
'rounded-box': 'crop_square',
|
|
47
47
|
'hollow-box': 'check_box_outline_blank',
|
|
48
48
|
polygon: 'pentagon',
|
|
49
|
+
pyramid: 'details',
|
|
49
50
|
portal: 'door_front'
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -62,6 +63,7 @@ const RECIPES: Array<{ kind: FigurePaletteKind; icon: string; label: string }> =
|
|
|
62
63
|
{ kind: 'sphere', icon: 'circle', label: 'figure.label.primitive-sphere' },
|
|
63
64
|
{ kind: 'frustum', icon: 'change_history', label: 'figure.label.primitive-frustum' },
|
|
64
65
|
{ kind: 'polygon', icon: 'hexagon', label: 'figure.label.primitive-polygon' },
|
|
66
|
+
{ kind: 'pyramid', icon: 'details', label: 'figure.label.primitive-pyramid' },
|
|
65
67
|
{ kind: 'plate', icon: 'check_box_outline_blank', label: 'figure.label.primitive-plate' },
|
|
66
68
|
{ kind: 'wall', icon: 'crop_portrait', label: 'figure.label.primitive-wall' },
|
|
67
69
|
{ kind: 'portal', icon: 'door_front', label: 'figure.label.primitive-portal' }
|
|
@@ -6,7 +6,7 @@ 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 { insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf, type SectionWorking } from './section-drawing.js'
|
|
9
|
+
import { apexOver, drawnOf, insertAfter, markedBy, movePoint, pointLabel, removePoint, sectionOf, workingIssuesOf, workingOf, type SectionWorking } from './section-drawing.js'
|
|
10
10
|
import { END_FACE } from './section-controls.js'
|
|
11
11
|
|
|
12
12
|
type Axis = 'x' | 'y' | 'z'
|
|
@@ -28,6 +28,10 @@ export const NARROW_BELOW = 900
|
|
|
28
28
|
* part's placement or fastening keeps (§4.1: read here, changed elsewhere). A measure that follows another value is
|
|
29
29
|
* shown and not offered.
|
|
30
30
|
*
|
|
31
|
+
* A drawn section's end can be drawn to one point -- a pyramid, a tetrahedron from three points (§4.1.1, A5-1): the
|
|
32
|
+
* apex is dragged or typed like a point, and is not offered while something is fastened to the end it takes away (the
|
|
33
|
+
* kernel's own refusal, `pointBlocked`).
|
|
34
|
+
*
|
|
31
35
|
* What the kernel would refuse is marked on the outline and said in the list; apply is held back, and the solid shows
|
|
32
36
|
* the last valid state, which the caption names. Apply sends one `set-section` command (one step of the history); cancel
|
|
33
37
|
* or Esc sends nothing and lets the preview go. The undo here is the editor's own. If the figure changes while the editor
|
|
@@ -150,6 +154,15 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
150
154
|
min-width: 0;
|
|
151
155
|
font: inherit;
|
|
152
156
|
}
|
|
157
|
+
label[apex] {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 6px;
|
|
161
|
+
grid-column: 1 / -1;
|
|
162
|
+
}
|
|
163
|
+
label[apex] small {
|
|
164
|
+
flex-basis: 100%;
|
|
165
|
+
}
|
|
153
166
|
ol[points] {
|
|
154
167
|
list-style: none;
|
|
155
168
|
margin: 0;
|
|
@@ -226,6 +239,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
226
239
|
@property({ attribute: false }) measures?: { measures: Record<Axis, FigureSectionMeasure>; round: boolean }
|
|
227
240
|
/** Along the push, the face a length change keeps: 1 its start, -1 its end, 0 its centre; undefined when it cannot be said. */
|
|
228
241
|
@property({ attribute: false }) kept?: -1 | 0 | 1
|
|
242
|
+
/** Why a push's end cannot be drawn to a point now, in the kernel's words (what is fastened to it); empty when it can. */
|
|
243
|
+
@property({ attribute: false }) pointBlocked = ''
|
|
229
244
|
/** The figure's revision now; the editor remembers the one it opened at. */
|
|
230
245
|
@property({ type: Number }) revision = 0
|
|
231
246
|
/** Narrow: the outline or the solid behind. */
|
|
@@ -237,7 +252,7 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
237
252
|
if (!this.narrow) this.show = 'outline'
|
|
238
253
|
})
|
|
239
254
|
|
|
240
|
-
@state() private working: SectionWorking = { points: [], across: {}, length: 0 }
|
|
255
|
+
@state() private working: SectionWorking = { points: [], across: {}, length: 0, apex: null }
|
|
241
256
|
@state() private selected: string | null = null
|
|
242
257
|
@state() private note = ''
|
|
243
258
|
private opened!: SectionWorking
|
|
@@ -250,10 +265,10 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
250
265
|
private extent = 200
|
|
251
266
|
|
|
252
267
|
private get axis(): Axis {
|
|
253
|
-
return (this.body
|
|
268
|
+
return ((this.body && drawnOf(this.body.shape)?.axis) ?? 'y') as Axis
|
|
254
269
|
}
|
|
255
270
|
private get kind() {
|
|
256
|
-
return this.body
|
|
271
|
+
return this.body && drawnOf(this.body.shape)?.section.kind
|
|
257
272
|
}
|
|
258
273
|
|
|
259
274
|
connectedCallback() {
|
|
@@ -276,7 +291,12 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
276
291
|
/** How far the drawing reaches from the origin: the outline's farthest point with room around, in 50 mm steps. */
|
|
277
292
|
private extentOf(w: SectionWorking): number {
|
|
278
293
|
const [h, v] = SCREEN[this.axis]
|
|
279
|
-
const far = Math.max(
|
|
294
|
+
const far = Math.max(
|
|
295
|
+
1,
|
|
296
|
+
...w.points.flatMap(p => [Math.abs(p[h] ?? 0), Math.abs(p[v] ?? 0)]),
|
|
297
|
+
...Object.values(w.across).map(a => (a ?? 0) / 2),
|
|
298
|
+
...Object.values(w.apex ?? {}).map(a => Math.abs(a ?? 0))
|
|
299
|
+
)
|
|
280
300
|
return Math.ceil((far * 1.4 + 40) / 50) * 50
|
|
281
301
|
}
|
|
282
302
|
|
|
@@ -336,12 +356,14 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
336
356
|
const o = this.opened
|
|
337
357
|
const changed = (a: number | undefined, b: number | undefined) => Math.abs((a ?? 0) - (b ?? 0)) > 1e-9
|
|
338
358
|
const across = Object.fromEntries(Object.entries(w.across).filter(([k, v]) => changed(v, o.across[k as Axis])))
|
|
359
|
+
const apexMoved = !w.apex !== !o.apex || (!!w.apex && Object.entries(w.apex).some(([k, v]) => changed(v, o.apex![k as Axis])))
|
|
339
360
|
return {
|
|
340
361
|
kind: 'set-section',
|
|
341
362
|
part: this.body!.id,
|
|
342
363
|
...(this.kind === 'polygon' ? { points: w.points.map(p => ({ ...p })) } : {}),
|
|
343
364
|
...(Object.keys(across).length ? { across } : {}),
|
|
344
|
-
...(changed(w.length, o.length) ? { length: w.length } : {})
|
|
365
|
+
...(changed(w.length, o.length) ? { length: w.length } : {}),
|
|
366
|
+
...(apexMoved ? { apex: w.apex ? { ...w.apex } : null } : {})
|
|
345
367
|
}
|
|
346
368
|
}
|
|
347
369
|
|
|
@@ -469,7 +491,9 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
469
491
|
const handle = /^handle:(.)$/.exec(this.dragging)
|
|
470
492
|
this.working = handle
|
|
471
493
|
? this.across(handle[1] as Axis, Math.max(1, 2 * Math.abs(handle[1] === h ? p.h : p.v)))
|
|
472
|
-
:
|
|
494
|
+
: this.dragging === 'apex'
|
|
495
|
+
? { ...this.working, apex: { [h]: p.h, [v]: p.v } }
|
|
496
|
+
: { ...this.working, points: movePoint(this.working.points, this.dragging, { [h]: p.h, [v]: p.v }) }
|
|
473
497
|
if (this.valid(this.working)) this.lastValid = this.working
|
|
474
498
|
this.preview()
|
|
475
499
|
}}
|
|
@@ -523,6 +547,22 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
523
547
|
fill=${bad ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-on-surface)'}>${pointLabel(p.id)}</text>
|
|
524
548
|
</g>`
|
|
525
549
|
})}
|
|
550
|
+
${w.apex
|
|
551
|
+
? svg`${pts.map(p => svg`<line x1=${p[h]} y1=${p[v]} x2=${w.apex![h]} y2=${w.apex![v]} stroke="var(--md-sys-color-primary)" stroke-width=${e / 300} stroke-dasharray=${`${e / 60} ${e / 90}`} />`)}
|
|
552
|
+
<g apex style="cursor: grab"
|
|
553
|
+
@pointerdown=${(ev: PointerEvent) => {
|
|
554
|
+
ev.stopPropagation()
|
|
555
|
+
;(ev.target as Element).setPointerCapture?.(ev.pointerId)
|
|
556
|
+
this.dragFrom = this.working
|
|
557
|
+
this.selected = null
|
|
558
|
+
this.dragging = 'apex'
|
|
559
|
+
}}>
|
|
560
|
+
<rect x=${w.apex[h]! - e / 34} y=${w.apex[v]! - e / 34} width=${e / 17} height=${e / 17}
|
|
561
|
+
transform=${`rotate(45 ${w.apex[h]} ${w.apex[v]})`} fill="var(--md-sys-color-tertiary)" stroke="var(--md-sys-color-surface)" stroke-width=${e / 250} />
|
|
562
|
+
<text x=${w.apex[h]! + e / 22} y=${w.apex[v]! + e / 12} font-size=${e / 16} font-weight="600" fill="var(--md-sys-color-on-surface)">${i18next.t('figure.label.section-editor-apex-mark')}</text>
|
|
563
|
+
<title>${i18next.t('figure.label.section-editor-apex-mark')}</title>
|
|
564
|
+
</g>`
|
|
565
|
+
: nothing}
|
|
526
566
|
${this.kind === 'polygon'
|
|
527
567
|
? nothing
|
|
528
568
|
: ([h, v] as const).filter(sized).map(axis => {
|
|
@@ -571,6 +611,50 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
571
611
|
</label>`
|
|
572
612
|
}
|
|
573
613
|
|
|
614
|
+
/** A drawn section's end: the section again (a push) or one point (a pyramid), and where that point is. */
|
|
615
|
+
private apexFields() {
|
|
616
|
+
const [h, v] = SCREEN[this.axis]
|
|
617
|
+
const apex = this.working.apex
|
|
618
|
+
/* The kernel's refusal holds only for turning a push into a pyramid; a pyramid can always be made a push again. */
|
|
619
|
+
const blocked = !apex && !this.opened?.apex ? this.pointBlocked : ''
|
|
620
|
+
return html`<label apex>
|
|
621
|
+
<input
|
|
622
|
+
type="checkbox"
|
|
623
|
+
.checked=${live(!!apex)}
|
|
624
|
+
?disabled=${!!blocked}
|
|
625
|
+
@change=${(ev: Event) => {
|
|
626
|
+
const on = (ev.target as HTMLInputElement).checked
|
|
627
|
+
this.change({ ...this.working, apex: on ? (this.opened.apex ?? apexOver(this.working.points, this.axis)) : null })
|
|
628
|
+
}}
|
|
629
|
+
/>
|
|
630
|
+
<span>${i18next.t('figure.label.section-editor-apex')}</span>
|
|
631
|
+
${blocked ? html`<small>${blocked}</small>` : nothing}
|
|
632
|
+
</label>
|
|
633
|
+
${apex
|
|
634
|
+
? ([h, v] as const).map(axis =>
|
|
635
|
+
html`<label mm>
|
|
636
|
+
<span>${i18next.t('figure.label.section-editor-apex-at', { axis: axis.toUpperCase() })}</span>
|
|
637
|
+
<input
|
|
638
|
+
type="number"
|
|
639
|
+
step="any"
|
|
640
|
+
inputmode="decimal"
|
|
641
|
+
aria-label=${i18next.t('figure.label.section-editor-apex-at', { axis: axis.toUpperCase() })}
|
|
642
|
+
.value=${live(String(apex[axis] ?? ''))}
|
|
643
|
+
@change=${(ev: Event) => {
|
|
644
|
+
const input = ev.target as HTMLInputElement
|
|
645
|
+
const value = Number(input.value)
|
|
646
|
+
if (input.value === '' || !Number.isFinite(value)) {
|
|
647
|
+
input.value = String(apex[axis] ?? '')
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
this.change({ ...this.working, apex: { ...apex, [axis]: value } })
|
|
651
|
+
}}
|
|
652
|
+
/>
|
|
653
|
+
</label>`
|
|
654
|
+
)
|
|
655
|
+
: nothing}`
|
|
656
|
+
}
|
|
657
|
+
|
|
574
658
|
private list(issues: ReturnType<typeof workingIssuesOf>['issues'], words: ReturnType<typeof workingIssuesOf>['words']) {
|
|
575
659
|
const [h, v] = SCREEN[this.axis]
|
|
576
660
|
const marked = markedBy(issues)
|
|
@@ -578,12 +662,13 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
578
662
|
const linked = i18next.t('figure.text.section-editor-linked')
|
|
579
663
|
return html`<div sizes>
|
|
580
664
|
${this.field(
|
|
581
|
-
i18next.t('figure.label.section-editor-length'),
|
|
665
|
+
i18next.t(this.working.apex ? 'figure.label.section-editor-height' : 'figure.label.section-editor-length'),
|
|
582
666
|
this.working.length,
|
|
583
667
|
value => this.change({ ...this.working, length: value }),
|
|
584
668
|
m?.[this.axis] !== 'editable',
|
|
585
669
|
m?.[this.axis] === 'linked' ? linked : this.keptWords()
|
|
586
670
|
)}
|
|
671
|
+
${this.kind === 'polygon' ? this.apexFields() : nothing}
|
|
587
672
|
${this.kind === 'polygon'
|
|
588
673
|
? nothing
|
|
589
674
|
: this.measures?.round
|
|
@@ -5,6 +5,7 @@ import { live } from 'lit/directives/live.js'
|
|
|
5
5
|
import { i18next, localize } from '@operato/i18n'
|
|
6
6
|
import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model'
|
|
7
7
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
8
|
+
import { drawnOf } from './section-drawing.js'
|
|
8
9
|
import {
|
|
9
10
|
END_FACE,
|
|
10
11
|
SIDE_FACE,
|
|
@@ -94,7 +95,8 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
|
|
|
94
95
|
|
|
95
96
|
render() {
|
|
96
97
|
const body = this.body
|
|
97
|
-
|
|
98
|
+
/* A push, or a pyramid: a drawn section drawn to one point, which has no corners and no inside. */
|
|
99
|
+
const e = body?.shape ? drawnOf(body.shape) : undefined
|
|
98
100
|
if (!body || !e) return nothing
|
|
99
101
|
const kind = e.section.kind
|
|
100
102
|
return html`<section>
|
|
@@ -173,7 +173,14 @@ export function shapeKeyOf(body: FigureAssetGeometry | FigureGeometry): string {
|
|
|
173
173
|
const e = shape.extrusion,
|
|
174
174
|
c = shape.cavity,
|
|
175
175
|
r = shape.revolution
|
|
176
|
-
const
|
|
176
|
+
const l = shape.loft
|
|
177
|
+
const meaning = e
|
|
178
|
+
? `${e.axis}:${e.section.kind}:${c ? `${c.start === 'open' ? 'o' : 'c'}${c.end === 'open' ? 'o' : 'c'}` : '-'}`
|
|
179
|
+
: r
|
|
180
|
+
? `${r.axis}:${r.profile.kind}`
|
|
181
|
+
: l
|
|
182
|
+
? `${l.axis}:point`
|
|
183
|
+
: ''
|
|
177
184
|
const segments = (body as { segments?: number }).segments ?? 12
|
|
178
185
|
return `${shape.provider}|${meaning}|${dims}|s${segments}`
|
|
179
186
|
}
|
|
@@ -30,6 +30,19 @@ export function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimit
|
|
|
30
30
|
{ x: 0, z: 43 }
|
|
31
31
|
]
|
|
32
32
|
}
|
|
33
|
+
case 'pyramid':
|
|
34
|
+
/* A tetrahedron: the drawn triangle's, drawn up to one point over its middle. */
|
|
35
|
+
return {
|
|
36
|
+
...standing(100),
|
|
37
|
+
kind,
|
|
38
|
+
dimensions: { height: 100 },
|
|
39
|
+
points: [
|
|
40
|
+
{ x: -50, z: -43 },
|
|
41
|
+
{ x: 50, z: -43 },
|
|
42
|
+
{ x: 0, z: 43 }
|
|
43
|
+
],
|
|
44
|
+
apex: { x: 0, z: 0 }
|
|
45
|
+
}
|
|
33
46
|
case 'plate':
|
|
34
47
|
return { ...standing(10), kind: 'box', dimensions: { width: 600, height: 10, depth: 400 } }
|
|
35
48
|
case 'wall':
|
|
@@ -104,11 +104,29 @@ export interface SectionWorking {
|
|
|
104
104
|
/** Along each plane axis: a rectangle's size, an ellipse's diameter. Empty for a drawn section. */
|
|
105
105
|
across: Partial<Record<Axis, number>>
|
|
106
106
|
length: number
|
|
107
|
+
/** A drawn section's end drawn to one point (a pyramid), across the plane axes; null for a push. */
|
|
108
|
+
apex: Partial<Record<Axis, number>> | null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A pushed part's section, axis and ends -- or a pyramid's, whose end is its apex (figure-model `loft@1`). */
|
|
112
|
+
export function drawnOf(shape: FigureEvaluatedShape):
|
|
113
|
+
| { axis: Axis; section: FigureSection; start: number; end: number; apex: Partial<Record<Axis, number>> | null }
|
|
114
|
+
| undefined {
|
|
115
|
+
if (shape.extrusion) return { ...shape.extrusion, apex: null }
|
|
116
|
+
if (shape.loft) return { ...shape.loft, apex: shape.loft.apex }
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Where a new apex goes: over the middle of the points' span, whole millimetres. */
|
|
121
|
+
export function apexOver(points: Points, axis: Axis): Partial<Record<Axis, number>> {
|
|
122
|
+
const [u, v] = FIGURE_SECTION_PLANE[axis]
|
|
123
|
+
const mid = (x: Axis) => Math.round((Math.min(...points.map(p => p[x]!)) + Math.max(...points.map(p => p[x]!))) / 2)
|
|
124
|
+
return { [u]: mid(u), [v]: mid(v) }
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
/** The working state of a pushed part as it stands. */
|
|
110
128
|
export function workingOf(shape: FigureEvaluatedShape): SectionWorking {
|
|
111
|
-
const e = shape
|
|
129
|
+
const e = drawnOf(shape)!
|
|
112
130
|
const s = e.section
|
|
113
131
|
const [u, v] = FIGURE_SECTION_PLANE[e.axis]
|
|
114
132
|
const across =
|
|
@@ -117,13 +135,14 @@ export function workingOf(shape: FigureEvaluatedShape): SectionWorking {
|
|
|
117
135
|
: s.kind === 'ellipse'
|
|
118
136
|
? { [u]: 2 * s.radius[u]!, [v]: 2 * s.radius[v]! }
|
|
119
137
|
: {}
|
|
120
|
-
return { points: s.kind === 'polygon' ? s.points.map(p => ({ ...p })) : [], across, length: e.end - e.start }
|
|
138
|
+
return { points: s.kind === 'polygon' ? s.points.map(p => ({ ...p })) : [], across, length: e.end - e.start, apex: e.apex ? { ...e.apex } : null }
|
|
121
139
|
}
|
|
122
140
|
|
|
123
141
|
/** The section the working state makes, of the part's own kind: its corners kept, its points or widths the working ones. */
|
|
124
142
|
export function sectionOf(shape: FigureEvaluatedShape, w: SectionWorking): FigureSection {
|
|
125
|
-
const
|
|
126
|
-
const
|
|
143
|
+
const e = drawnOf(shape)!
|
|
144
|
+
const s = e.section
|
|
145
|
+
const [u, v] = FIGURE_SECTION_PLANE[e.axis]
|
|
127
146
|
if (s.kind === 'polygon') return { kind: 'polygon', points: w.points }
|
|
128
147
|
if (s.kind === 'ellipse') return { kind: 'ellipse', radius: { [u]: w.across[u]! / 2, [v]: w.across[v]! / 2 } }
|
|
129
148
|
return { kind: 'rectangle', size: { [u]: w.across[u]!, [v]: w.across[v]! }, ...(s.corner ? { corner: s.corner } : {}) }
|
|
@@ -134,11 +153,12 @@ export function workingIssuesOf(
|
|
|
134
153
|
shape: FigureEvaluatedShape,
|
|
135
154
|
w: SectionWorking
|
|
136
155
|
): { issues: FigureSectionIssue[]; words: Array<{ key: string; params: Record<string, string> }> } {
|
|
137
|
-
const e = shape
|
|
156
|
+
const e = drawnOf(shape)!
|
|
138
157
|
const section = sectionOf(shape, w)
|
|
139
158
|
const issues = checkFigureSection(section, e.axis)
|
|
140
159
|
const words = issues.map(i => issueWords(i, w.points, e.axis))
|
|
141
160
|
if (!(Number.isFinite(w.length) && w.length > 0)) words.push({ key: 'figure.text.section-issue-length', params: {} })
|
|
161
|
+
if (w.apex && !Object.values(w.apex).every(n => Number.isFinite(n))) words.push({ key: 'figure.text.section-issue-number', params: {} })
|
|
142
162
|
if (!issues.length && words.length === 0 && shape.cavity) {
|
|
143
163
|
const made = figureExtrusionFeaturesOf({ axis: e.axis, section, start: e.start, end: e.start + w.length }, [shape.cavity])
|
|
144
164
|
if ('refused' in made) {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What a person calls a part's shape (ADR-0100 §2.1): the starting shape it reads as -- 상자, 둥근 상자, 위가 열린 상자,
|
|
3
|
-
* 원기둥, 문틀, 구, 원뿔, 그린
|
|
3
|
+
* 원기둥, 문틀, 구, 원뿔, 그린 단면, 뿔. A word for the screen, read from the shape node's own params; it is not a kind of
|
|
4
4
|
* stored shape, and nothing is decided by it.
|
|
5
5
|
*/
|
|
6
|
-
export type StartingShapeKind = 'cube' | 'rounded-box' | 'hollow-box' | 'cylinder' | 'portal' | 'sphere' | 'frustum' | 'polygon'
|
|
6
|
+
export type StartingShapeKind = 'cube' | 'rounded-box' | 'hollow-box' | 'cylinder' | 'portal' | 'sphere' | 'frustum' | 'polygon' | 'pyramid'
|
|
7
7
|
|
|
8
8
|
export function startingShapeKindOf(shape: { op: string; params?: Record<string, unknown> } | undefined): StartingShapeKind | undefined {
|
|
9
9
|
const p = shape?.params ?? {}
|
|
10
10
|
if (shape?.op === 'revolve@1') return p.profile === 'taper' ? 'frustum' : 'sphere'
|
|
11
|
+
if (shape?.op === 'loft@1') return 'pyramid'
|
|
11
12
|
if (shape?.op !== 'extrude@1') return undefined
|
|
12
13
|
if (p.section === 'ellipse') return 'cylinder'
|
|
13
14
|
if (p.section === 'polygon') return 'polygon'
|
|
@@ -20,7 +21,8 @@ export function startingShapeKindOf(shape: { op: string; params?: Record<string,
|
|
|
20
21
|
* The same word for an evaluated body, when only the shape the kernel evaluated is in hand (a row of the parts list):
|
|
21
22
|
* a door frame is the section with jambs, a drawn section the rest of the drawn ones.
|
|
22
23
|
*/
|
|
23
|
-
export function startingShapeKindOfBody(shape: { provider?: string; dimensions?: Record<string, number>; extrusion?: { section: { kind: string; corner?: unknown } }; cavity?: unknown; revolution?: { profile: { kind: string } } } | undefined): StartingShapeKind | undefined {
|
|
24
|
+
export function startingShapeKindOfBody(shape: { provider?: string; dimensions?: Record<string, number>; extrusion?: { section: { kind: string; corner?: unknown } }; cavity?: unknown; revolution?: { profile: { kind: string } }; loft?: unknown } | undefined): StartingShapeKind | undefined {
|
|
25
|
+
if (shape?.loft) return 'pyramid'
|
|
24
26
|
if (shape?.revolution) return shape.revolution.profile.kind === 'taper' ? 'frustum' : 'sphere'
|
|
25
27
|
const s = shape?.extrusion?.section
|
|
26
28
|
if (!s) return undefined
|
|
@@ -295,6 +295,12 @@ export declare class FigureEditor extends LitElement {
|
|
|
295
295
|
*/
|
|
296
296
|
/** The section editor over the canvas, the figure behind it drawing what the working points make. */
|
|
297
297
|
private renderSectionEditor;
|
|
298
|
+
/**
|
|
299
|
+
* Why a drawn push's end cannot be drawn to one point now: the kernel's own refusal of that command on a copy (what is
|
|
300
|
+
* fastened to the end face), said as the face. Asked once per figure revision and part.
|
|
301
|
+
*/
|
|
302
|
+
private pointBlocked?;
|
|
303
|
+
private pointBlockedOf;
|
|
298
304
|
private renderStart;
|
|
299
305
|
/**
|
|
300
306
|
* Where the panel is, held at its top: the model by its name -- the way back to its settings -- and the part chosen,
|
|
@@ -32,6 +32,8 @@ import { SidePanelStyles } from './side-panel-styles.js';
|
|
|
32
32
|
import { ScrollbarStyles } from '@operato/styles';
|
|
33
33
|
import { alignChanges, boxOf, canMirror, mirrorChanges } from './align.js';
|
|
34
34
|
import { paletteRecipe } from './palette.js';
|
|
35
|
+
import { apexOver, drawnOf } from './section-drawing.js';
|
|
36
|
+
import { END_FACE } from './section-controls.js';
|
|
35
37
|
import { nudgeOf, placementOf } from './stage-model.js';
|
|
36
38
|
import { roomRefitRowsOf } from './occupancy.js';
|
|
37
39
|
import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './refusals.js';
|
|
@@ -1824,21 +1826,40 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1824
1826
|
renderSectionEditor(geometry) {
|
|
1825
1827
|
const part = geometry.find(g => ownerOf(g.id) === this.selected);
|
|
1826
1828
|
/* A door frame's section is its own width, height, jamb and head: its fields edit it, not an outline. */
|
|
1827
|
-
|
|
1829
|
+
const drawn = part?.shape && drawnOf(part.shape);
|
|
1830
|
+
if (!part || !drawn || part.shape.dimensions.jamb !== undefined)
|
|
1828
1831
|
return nothing;
|
|
1829
1832
|
const asset = this.asset();
|
|
1830
1833
|
const measures = figureSectionMeasuresOf(asset, this.selected);
|
|
1831
|
-
const kept = figureScaleAnchorOf(asset, this.selected)?.[
|
|
1834
|
+
const kept = figureScaleAnchorOf(asset, this.selected)?.[drawn.axis];
|
|
1832
1835
|
return html `<figure-section-editor
|
|
1833
1836
|
.body=${{ ...part, id: this.selected }}
|
|
1834
1837
|
.measures=${measures}
|
|
1835
1838
|
.kept=${kept}
|
|
1839
|
+
.pointBlocked=${this.pointBlockedOf(drawn)}
|
|
1836
1840
|
.revision=${this.session?.read().revision ?? 0}
|
|
1837
1841
|
@command=${(e) => this.linkCommand(e.detail.action)}
|
|
1838
1842
|
@command-live=${(e) => this.holdAction(e.detail.action)}
|
|
1839
1843
|
@section-editor-close=${() => this.closeSectionEditor()}
|
|
1840
1844
|
></figure-section-editor>`;
|
|
1841
1845
|
}
|
|
1846
|
+
pointBlockedOf(drawn) {
|
|
1847
|
+
if (drawn.apex || drawn.section.kind !== 'polygon' || !this.session)
|
|
1848
|
+
return '';
|
|
1849
|
+
const key = `${this.session.read().revision}|${this.selected}`;
|
|
1850
|
+
if (this.pointBlocked?.key === key)
|
|
1851
|
+
return this.pointBlocked.words;
|
|
1852
|
+
let words = '';
|
|
1853
|
+
try {
|
|
1854
|
+
this.session.previewCommand({ kind: 'set-section', part: this.selected, apex: apexOver(drawn.section.points, drawn.axis) });
|
|
1855
|
+
}
|
|
1856
|
+
catch (error) {
|
|
1857
|
+
if (error.code === 'FEATURE_MAPPING_REQUIRED')
|
|
1858
|
+
words = i18next.t('figure.text.section-editor-apex-blocked', { face: i18next.t('figure.label.face-' + END_FACE(drawn.axis, 'end')) });
|
|
1859
|
+
}
|
|
1860
|
+
this.pointBlocked = { key, words };
|
|
1861
|
+
return words;
|
|
1862
|
+
}
|
|
1842
1863
|
renderStart() {
|
|
1843
1864
|
const shapes = ['box', 'cylinder', 'plate'];
|
|
1844
1865
|
/* Where it is authored from, chosen before any part (ADR-0097): the figure's own setting, no room made up for it. */
|
|
@@ -2263,9 +2284,12 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
2263
2284
|
renderSingle(name, field, unavailable) {
|
|
2264
2285
|
/* A drawn section's point is called by its number and axis, as the section editor calls it (ADR-0100 §2.1, Q9). */
|
|
2265
2286
|
const point = /^point\.p(\d+)\.([xyz])$/.exec(name);
|
|
2287
|
+
const apex = /^apex\.([xyz])$/.exec(name);
|
|
2266
2288
|
const words = point
|
|
2267
2289
|
? [i18next.t('figure.label.section-editor-coordinate', { point: point[1], axis: point[2].toUpperCase() })]
|
|
2268
|
-
:
|
|
2290
|
+
: apex
|
|
2291
|
+
? [`${i18next.t('figure.label.section-editor-apex-mark')} ${apex[1].toUpperCase()}`]
|
|
2292
|
+
: name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }));
|
|
2269
2293
|
return html `<span ?scrub=${!unavailable} @pointerdown=${(e) => this.scrub(field, e, unavailable)}>${words.join(' · ')} (${field.unit})</span
|
|
2270
2294
|
><input
|
|
2271
2295
|
aria-label=${field.id}
|