@things-factory/figure-ui 10.1.147 → 10.1.149
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/bootstrap.ts +10 -2
- package/client/graphql/index.ts +20 -0
- package/client/modeller/canvas.ts +6 -2
- package/client/modeller/carrier-declaration.ts +63 -0
- package/client/modeller/editor.ts +62 -27
- package/client/modeller/figure-carrier.ts +117 -0
- package/client/modeller/figure-cavity-drawing.ts +3 -9
- package/client/modeller/figure-first-save.ts +4 -7
- package/client/modeller/figure-history-panel.ts +2 -3
- package/client/modeller/figure-links.ts +46 -60
- package/client/modeller/figure-material-panel.ts +4 -14
- package/client/modeller/figure-mesh-choice.ts +68 -0
- package/client/modeller/figure-parts.ts +23 -1
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-properties.ts +5 -3
- package/client/modeller/figure-section-editor.ts +12 -33
- package/client/modeller/figure-section-panel.ts +6 -7
- package/client/modeller/figure-slot-layout-panel.ts +208 -0
- package/client/modeller/figure-verdict.ts +21 -31
- package/client/modeller/first-save.ts +23 -0
- package/client/modeller/links.ts +52 -1
- package/client/modeller/material-edit.ts +38 -0
- package/client/modeller/maturity.ts +28 -0
- package/client/modeller/mesh-import.ts +77 -0
- package/client/modeller/modeller-route.ts +9 -0
- package/client/modeller/mounting-part.ts +15 -0
- package/client/modeller/newest-answer.ts +21 -0
- package/client/modeller/preview-model.ts +5 -0
- package/client/modeller/refusals.ts +168 -82
- package/client/modeller/section-controls.ts +25 -1
- package/client/modeller/section-drawing.ts +43 -1
- package/client/modeller/slot-layout-host.ts +140 -0
- package/client/pages/figure-modeller-page.ts +6 -5
- package/client/viewparts/detail-route.ts +7 -0
- package/client/viewparts/figure-detail-dock.ts +1 -3
- package/dist-client/bootstrap.d.ts +1 -1
- package/dist-client/bootstrap.js +9 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/graphql/index.d.ts +5 -0
- package/dist-client/graphql/index.js +19 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +2 -0
- package/dist-client/modeller/canvas.js +10 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/carrier-declaration.d.ts +31 -0
- package/dist-client/modeller/carrier-declaration.js +53 -0
- package/dist-client/modeller/carrier-declaration.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +13 -0
- package/dist-client/modeller/editor.js +62 -27
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-carrier.d.ts +26 -0
- package/dist-client/modeller/figure-carrier.js +123 -0
- package/dist-client/modeller/figure-carrier.js.map +1 -0
- package/dist-client/modeller/figure-cavity-drawing.js +3 -11
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +4 -0
- package/dist-client/modeller/figure-first-save.js +5 -8
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -4
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -1
- package/dist-client/modeller/figure-links.js +47 -57
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +0 -1
- package/dist-client/modeller/figure-material-panel.js +4 -14
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
- package/dist-client/modeller/figure-mesh-choice.js +75 -0
- package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +22 -0
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +2 -1
- package/dist-client/modeller/figure-properties.js +5 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +2 -2
- package/dist-client/modeller/figure-section-editor.js +12 -33
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +6 -7
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
- package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +1 -1
- package/dist-client/modeller/figure-verdict.js +19 -37
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +10 -0
- package/dist-client/modeller/first-save.js +16 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/links.d.ts +26 -1
- package/dist-client/modeller/links.js +43 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +11 -0
- package/dist-client/modeller/material-edit.js +28 -0
- package/dist-client/modeller/material-edit.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +15 -0
- package/dist-client/modeller/maturity.js +23 -0
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +47 -0
- package/dist-client/modeller/mesh-import.js +47 -0
- package/dist-client/modeller/mesh-import.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +5 -0
- package/dist-client/modeller/modeller-route.js +9 -0
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/mounting-part.d.ts +14 -0
- package/dist-client/modeller/mounting-part.js +10 -0
- package/dist-client/modeller/mounting-part.js.map +1 -0
- package/dist-client/modeller/newest-answer.d.ts +12 -0
- package/dist-client/modeller/newest-answer.js +21 -0
- package/dist-client/modeller/newest-answer.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +2 -0
- package/dist-client/modeller/preview-model.js +4 -0
- package/dist-client/modeller/preview-model.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +26 -5
- package/dist-client/modeller/refusals.js +184 -86
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.d.ts +17 -1
- package/dist-client/modeller/section-controls.js +18 -0
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +24 -1
- package/dist-client/modeller/section-drawing.js +42 -0
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +81 -0
- package/dist-client/modeller/slot-layout-host.js +105 -0
- package/dist-client/modeller/slot-layout-host.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.js +5 -4
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/detail-route.d.ts +5 -0
- package/dist-client/viewparts/detail-route.js +8 -0
- package/dist-client/viewparts/detail-route.js.map +1 -0
- package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
- package/dist-client/viewparts/figure-detail-dock.js +1 -3
- package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
- package/package.json +9 -8
- package/test/align.test.ts +0 -9
- package/test/browser/assistant-stand-in.js +32 -0
- package/test/browser/build.mjs +6 -1
- package/test/browser/editing-page.js +224 -0
- package/test/browser/editing.check.mjs +550 -0
- package/test/browser/links-page.js +113 -0
- package/test/browser/links.check.mjs +568 -0
- package/test/browser/modeller.check.mjs +21 -0
- package/test/browser/parts-page.js +120 -0
- package/test/browser/parts.check.mjs +328 -0
- package/test/browser/places-mesh-page.js +36 -0
- package/test/browser/places-mesh.check.mjs +239 -0
- package/test/browser/product-page.check.mjs +682 -0
- package/test/browser/product-page.js +276 -0
- package/test/browser/screen-page.js +112 -0
- package/test/browser/screen-product-page.js +62 -0
- package/test/browser/screen.check.mjs +1003 -0
- package/test/browser/section-page.js +256 -0
- package/test/browser/section.check.mjs +719 -0
- package/test/browser/serve.mjs +20 -12
- package/test/carrier-declaration.test.ts +69 -0
- package/test/cavity-drawing.test.ts +2 -18
- package/test/clip-ai-proposal.test.ts +1 -5
- package/test/clip-consent.test.ts +0 -5
- package/test/clips.test.ts +0 -7
- package/test/command-preview.test.ts +0 -68
- package/test/controls.test.ts +0 -9
- package/test/detail-dock.test.ts +6 -25
- package/test/face-mapping.test.ts +1 -20
- package/test/fasten-drawing.test.ts +0 -23
- package/test/fields.test.ts +6 -33
- package/test/figure-assistant-surface.test.ts +32 -38
- package/test/first-minute.test.ts +3 -55
- package/test/first-save-suggest.test.ts +11 -8
- package/test/first-save.test.ts +4 -26
- package/test/gizmo.test.ts +0 -25
- package/test/joint-fan.test.ts +0 -10
- package/test/keys.test.ts +0 -7
- package/test/layout.test.ts +4 -73
- package/test/links.test.ts +48 -67
- package/test/live.test.ts +0 -34
- package/test/material.test.ts +17 -26
- package/test/maturity.test.ts +13 -16
- package/test/mesh-import.test.ts +76 -0
- package/test/modeller-route.test.ts +3 -19
- package/test/package-download.test.ts +6 -7
- package/test/parts-wiring.test.ts +9 -51
- package/test/preview.test.ts +5 -46
- package/test/properties.test.ts +11 -26
- package/test/proposal-record.test.ts +2 -19
- package/test/recipes.test.ts +0 -15
- package/test/refusals.test.ts +257 -151
- package/test/release-entry.test.ts +1 -9
- package/test/release-panel.test.ts +1 -22
- package/test/resize-with-placement.test.ts +0 -32
- package/test/revert-choice.test.ts +1 -22
- package/test/revert-keeping.test.ts +1 -12
- package/test/save-landed.test.ts +16 -14
- package/test/scene-canvas.test.ts +2 -13
- package/test/section-editor.test.ts +30 -83
- package/test/section-panel.test.ts +22 -17
- package/test/slot-layout-host.test.ts +117 -0
- package/test/theme.test.ts +5 -9
- package/test/verdict.test.ts +44 -41
- package/test/view.test.ts +5 -12
- package/translations/en.json +61 -1
- package/translations/es.json +61 -1
- package/translations/ja.json +61 -1
- package/translations/ko.json +61 -1
- package/translations/ms.json +61 -1
- package/translations/zh.json +61 -1
- package/test/thumbnail.test.ts +0 -29
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
|
+
import { i18next, localize } from '@operato/i18n'
|
|
4
|
+
import '@material/web/icon/icon.js'
|
|
5
|
+
import '@operato/slot-layout-editor'
|
|
6
|
+
import { compileFigureAsset, type FigureAsset, type SlotLayout } from '@hatiolab/figure-model'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
anchorFacesOf,
|
|
10
|
+
anchorIdFor,
|
|
11
|
+
figureSlotIssues,
|
|
12
|
+
slotLayoutCommandOf,
|
|
13
|
+
slotLayoutFormOf,
|
|
14
|
+
slotPresetsOf,
|
|
15
|
+
type SlotAnchor,
|
|
16
|
+
type SlotAnchorFace,
|
|
17
|
+
type SlotLayoutForm
|
|
18
|
+
} from './slot-layout-host.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A figure's slot layout (ADR-0118): places made by a rule -- anchors on parts' tops or bottoms, tiers on them, slots in
|
|
22
|
+
* the tiers. The tiers and slots are edited in the shared editor (`ox-slot-layout-editor`, the one fmsim hosts); this
|
|
23
|
+
* panel owns what only the modeller knows: which part's face each anchor is, the layout's name and what its slots are.
|
|
24
|
+
* Every change goes out as one `set-slot-layout` command -- one step of the history, refused by figure-model in its words.
|
|
25
|
+
*
|
|
26
|
+
* The layout here is the only one: a scene or a run never overrides it (ADR-0118 decision 8), so there is no "default".
|
|
27
|
+
*/
|
|
28
|
+
@customElement('figure-slot-layout-panel')
|
|
29
|
+
export class FigureSlotLayoutPanel extends localize(i18next)(LitElement) {
|
|
30
|
+
static styles = css`
|
|
31
|
+
:host {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 6px;
|
|
35
|
+
}
|
|
36
|
+
div[form] {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: auto 1fr;
|
|
39
|
+
gap: 4px 8px;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
ul {
|
|
43
|
+
list-style: none;
|
|
44
|
+
margin: 0;
|
|
45
|
+
padding: 0;
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
gap: 4px;
|
|
49
|
+
}
|
|
50
|
+
li {
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template-columns: 1fr 1fr auto auto;
|
|
53
|
+
gap: 4px;
|
|
54
|
+
align-items: center;
|
|
55
|
+
}
|
|
56
|
+
input,
|
|
57
|
+
select {
|
|
58
|
+
min-width: 0;
|
|
59
|
+
font: inherit;
|
|
60
|
+
}
|
|
61
|
+
div[actions] {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-wrap: wrap;
|
|
64
|
+
gap: 4px;
|
|
65
|
+
}
|
|
66
|
+
button {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 4px;
|
|
70
|
+
font: inherit;
|
|
71
|
+
}
|
|
72
|
+
md-icon {
|
|
73
|
+
--md-icon-size: 16px;
|
|
74
|
+
}
|
|
75
|
+
small[hint] {
|
|
76
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
77
|
+
}
|
|
78
|
+
`
|
|
79
|
+
|
|
80
|
+
@property({ attribute: false }) asset?: FigureAsset
|
|
81
|
+
/** The part selected on the canvas: what a new anchor stands on. */
|
|
82
|
+
@property() partId = ''
|
|
83
|
+
@property({ type: Boolean }) locked = false
|
|
84
|
+
|
|
85
|
+
/** The name, role and anchors being edited; started again from the figure whenever the figure changes. */
|
|
86
|
+
@state() private form?: SlotLayoutForm
|
|
87
|
+
private formFor?: FigureAsset
|
|
88
|
+
private options?: { key: string; asset: FigureAsset; value: object }
|
|
89
|
+
/*
|
|
90
|
+
The figure's layout, read once per figure: the editor starts its draft again whenever its value changes, so a value
|
|
91
|
+
read again every render would throw the author's edits away.
|
|
92
|
+
*/
|
|
93
|
+
private stored?: ReturnType<typeof slotLayoutFormOf>
|
|
94
|
+
|
|
95
|
+
willUpdate() {
|
|
96
|
+
/* A figure changed under the panel (an undo, another edit): what is shown is the figure's, not an old draft. */
|
|
97
|
+
if (this.asset !== this.formFor) {
|
|
98
|
+
this.formFor = this.asset
|
|
99
|
+
this.stored = this.asset ? slotLayoutFormOf(this.asset, this.partId) : undefined
|
|
100
|
+
this.form = this.stored?.form
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render() {
|
|
105
|
+
const asset = this.asset
|
|
106
|
+
const stored = this.stored
|
|
107
|
+
const form = this.form
|
|
108
|
+
if (!asset || !stored || !form) return nothing
|
|
109
|
+
const parts = asset.document.model.nodes.filter(n => n.op === 'place@1').map(n => n.id)
|
|
110
|
+
const changed = JSON.stringify(form) !== JSON.stringify(stored.form)
|
|
111
|
+
return html`
|
|
112
|
+
<div form>
|
|
113
|
+
<span>${i18next.t('figure.label.slot-layout-name')}</span>
|
|
114
|
+
<input .value=${form.label} placeholder=${i18next.t('figure.label.slot-layout')} ?disabled=${this.locked} @change=${(e: Event) => this.edit({ label: (e.target as HTMLInputElement).value })} />
|
|
115
|
+
<span>${i18next.t('figure.label.slot-layout-role')}</span>
|
|
116
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => this.edit({ role: (e.target as HTMLSelectElement).value as 'seat' | 'grip' })}>
|
|
117
|
+
<option value="seat" ?selected=${form.role === 'seat'}>${i18next.t('figure.label.place-seat')}</option>
|
|
118
|
+
<option value="grip" ?selected=${form.role === 'grip'}>${i18next.t('figure.label.place-grip')}</option>
|
|
119
|
+
</select>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<h4>${i18next.t('figure.label.slot-anchors')}</h4>
|
|
123
|
+
<ul>
|
|
124
|
+
${form.anchors.map(
|
|
125
|
+
(a, i) => html`<li>
|
|
126
|
+
<input .value=${a.id} aria-label=${i18next.t('figure.label.slot-anchor-id')} ?disabled=${this.locked} @change=${(e: Event) => this.anchor(i, { id: (e.target as HTMLInputElement).value.trim() })} />
|
|
127
|
+
<select aria-label=${i18next.t('figure.label.part')} ?disabled=${this.locked} @change=${(e: Event) => this.anchor(i, { part: (e.target as HTMLSelectElement).value })}>
|
|
128
|
+
${parts.map(p => html`<option value=${p} ?selected=${p === a.part}>${p}</option>`)}
|
|
129
|
+
</select>
|
|
130
|
+
<select aria-label=${i18next.t('figure.label.face')} ?disabled=${this.locked} @change=${(e: Event) => this.anchor(i, { face: (e.target as HTMLSelectElement).value as SlotAnchorFace })}>
|
|
131
|
+
${(['top', 'bottom'] as const).map(f => html`<option value=${f} ?selected=${f === a.face}>${i18next.t('figure.label.face-' + f)}</option>`)}
|
|
132
|
+
</select>
|
|
133
|
+
<button ?disabled=${this.locked || form.anchors.length <= 1} title=${i18next.t('figure.button.remove-slot-anchor')} @click=${() => this.edit({ anchors: form.anchors.filter((_, k) => k !== i) })}>
|
|
134
|
+
<md-icon>close</md-icon>
|
|
135
|
+
</button>
|
|
136
|
+
</li>`
|
|
137
|
+
)}
|
|
138
|
+
</ul>
|
|
139
|
+
<div actions>
|
|
140
|
+
${(['top', 'bottom'] as const).map(
|
|
141
|
+
face => html`<button ?disabled=${this.locked || !this.partId} @click=${() => this.addAnchor(face)}>
|
|
142
|
+
<md-icon>add</md-icon>${i18next.t(face === 'top' ? 'figure.button.add-slot-anchor-top' : 'figure.button.add-slot-anchor-bottom', { part: this.partId })}
|
|
143
|
+
</button>`
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
<small hint>${i18next.t('figure.text.slot-anchor-pick')}</small>
|
|
147
|
+
|
|
148
|
+
<ox-slot-layout-editor .value=${stored.layout} .options=${this.editorOptions(asset, form, stored.layout)} @change=${(e: Event) => this.apply((e.target as { value?: SlotLayout }).value)}></ox-slot-layout-editor>
|
|
149
|
+
|
|
150
|
+
<div actions>
|
|
151
|
+
${stored.layout && changed
|
|
152
|
+
? html`<button ?disabled=${this.locked} @click=${() => this.apply(stored.layout)}><md-icon>check</md-icon>${i18next.t('figure.button.apply-slot-anchors')}</button>`
|
|
153
|
+
: nothing}
|
|
154
|
+
${stored.layout
|
|
155
|
+
? html`<button ?disabled=${this.locked} @click=${() => this.command({ kind: 'remove-slot-layout' })}><md-icon>delete</md-icon>${i18next.t('figure.button.remove-slot-layout')}</button>`
|
|
156
|
+
: nothing}
|
|
157
|
+
</div>
|
|
158
|
+
`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
The editor's options, made again only when what they are made from changes: the editor starts its draft again when its
|
|
163
|
+
starting layout changes, so a new object every render would throw the author's edits away.
|
|
164
|
+
*/
|
|
165
|
+
private editorOptions(asset: FigureAsset, form: SlotLayoutForm, layout: SlotLayout | undefined) {
|
|
166
|
+
const key = JSON.stringify([form.anchors, !!layout])
|
|
167
|
+
if (this.options?.key === key && this.options.asset === asset) return this.options.value
|
|
168
|
+
const geometry = compileFigureAsset(asset).evaluate().geometry as never
|
|
169
|
+
const anchorFaces = anchorFacesOf(form.anchors, geometry)
|
|
170
|
+
const value = {
|
|
171
|
+
canUseDefault: false,
|
|
172
|
+
presets: slotPresetsOf(form.anchors, key => i18next.t(key)),
|
|
173
|
+
/* The first anchor's face is the size a preset is laid out over; the slot height is the author's. */
|
|
174
|
+
presetSize: anchorFaces[form.anchors[0]?.id ?? ''] ?? {},
|
|
175
|
+
anchors: form.anchors.map(a => ({ id: a.id, label: `${a.id} · ${a.part} ${i18next.t('figure.label.face-' + a.face)}` })),
|
|
176
|
+
anchorFaces,
|
|
177
|
+
validate: (layout: SlotLayout) => figureSlotIssues(layout, form.anchors.map(a => a.id)),
|
|
178
|
+
issueText: (issue: { code: string; path: string }) =>
|
|
179
|
+
issue.code === 'ACCEPTS_REQUIRED' ? `${issue.path}: ${i18next.t('figure.text.slot-accepts-required')}` : undefined
|
|
180
|
+
}
|
|
181
|
+
this.options = { key, asset, value }
|
|
182
|
+
return value
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private edit(patch: Partial<SlotLayoutForm>) {
|
|
186
|
+
this.form = { ...this.form!, ...patch }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private anchor(i: number, patch: Partial<SlotAnchor>) {
|
|
190
|
+
this.edit({ anchors: this.form!.anchors.map((a, k) => (k === i ? { ...a, ...patch } : a)) })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** A new anchor on the selected part's face: what an author picks on the canvas comes here. */
|
|
194
|
+
private addAnchor(face: SlotAnchorFace) {
|
|
195
|
+
const anchors = this.form!.anchors
|
|
196
|
+
this.edit({ anchors: [...anchors, { id: anchorIdFor(this.partId, anchors.map(a => a.id)), part: this.partId, face }] })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The whole layout with the anchors and name as they stand: one command, one step. */
|
|
200
|
+
private apply(layout: SlotLayout | undefined) {
|
|
201
|
+
if (!layout || !this.form) return
|
|
202
|
+
this.command(slotLayoutCommandOf(this.form, layout, i18next.t('figure.label.slot-layout')))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private command(action: object) {
|
|
206
|
+
this.dispatchEvent(new CustomEvent('command', { detail: { action }, bubbles: true, composed: true }))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -5,7 +5,7 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
|
5
5
|
|
|
6
6
|
import { i18next, localize } from '@operato/i18n'
|
|
7
7
|
import { ScrollbarStyles } from '@operato/styles'
|
|
8
|
-
import { LIMITS, FIGURE_PER_INSTANCE
|
|
8
|
+
import { LIMITS, FIGURE_PER_INSTANCE } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
import { inspectFigureDraft } from '../graphql/index.js'
|
|
11
11
|
import type { FigureInspection } from '../types.js'
|
|
@@ -13,7 +13,8 @@ import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
|
13
13
|
import { MEASURED } from './scene-budget.js'
|
|
14
14
|
import type { FigureRevision } from './revision.js'
|
|
15
15
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
16
|
-
import {
|
|
16
|
+
import { CURVE_SIZE, curveLayout, gradeBoundsOf, shortNumber, verdictMeasureOf } from './maturity.js'
|
|
17
|
+
import { newestAnswer } from './newest-answer.js'
|
|
17
18
|
import type { FigureBudgetUse, FigureMaturityAxis, FigureReport } from './maturity.js'
|
|
18
19
|
|
|
19
20
|
/** How long an edit settles before the server is asked again. One question per pause, not one per keystroke. */
|
|
@@ -400,7 +401,7 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
400
401
|
|
|
401
402
|
private timer?: ReturnType<typeof setTimeout>
|
|
402
403
|
/** Answers can come back out of order; only the newest question's answer is shown. */
|
|
403
|
-
private
|
|
404
|
+
private answers = newestAnswer<FigureInspection>()
|
|
404
405
|
|
|
405
406
|
disconnectedCallback() {
|
|
406
407
|
clearTimeout(this.timer)
|
|
@@ -423,21 +424,11 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
423
424
|
}
|
|
424
425
|
|
|
425
426
|
private measure() {
|
|
426
|
-
|
|
427
|
-
let bodies = 0
|
|
428
|
-
try {
|
|
429
|
-
const asset = this.revision?.asset
|
|
430
|
-
const cost = asset ? figureCostOf(asset) : undefined
|
|
431
|
-
bodies = cost ? cost.groups + cost.hidden : 0
|
|
432
|
-
report = asset ? figureReportOf(asset) : undefined
|
|
433
|
-
} catch {
|
|
434
|
-
/* A source the format refuses has no report; the server's finding below says why. */
|
|
435
|
-
report = undefined
|
|
436
|
-
}
|
|
427
|
+
const { report, bodies, told } = verdictMeasureOf(this.revision?.asset)
|
|
437
428
|
this.report = report
|
|
438
429
|
this.bodies = bodies
|
|
439
430
|
if (!report) this.expanded = false
|
|
440
|
-
this.dispatchEvent(new CustomEvent('capacity-changed', { detail:
|
|
431
|
+
this.dispatchEvent(new CustomEvent('capacity-changed', { detail: told }))
|
|
441
432
|
}
|
|
442
433
|
|
|
443
434
|
private schedule() {
|
|
@@ -450,22 +441,22 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
450
441
|
}
|
|
451
442
|
|
|
452
443
|
private async ask() {
|
|
453
|
-
const question = ++this.asked
|
|
454
444
|
const source = this.revision!.text
|
|
455
445
|
this.asking = true
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
446
|
+
await this.answers(
|
|
447
|
+
() => inspectFigureDraft(source, this.figureId),
|
|
448
|
+
outcome => {
|
|
449
|
+
this.asking = false
|
|
450
|
+
if ('answer' in outcome) {
|
|
451
|
+
this.inspection = outcome.answer
|
|
452
|
+
this.failure = ''
|
|
453
|
+
} else {
|
|
454
|
+
/* The server's reason as it gave it — the screen does not make one up. The last answer is dropped with it. */
|
|
455
|
+
this.inspection = undefined
|
|
456
|
+
this.failure = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
)
|
|
469
460
|
}
|
|
470
461
|
|
|
471
462
|
render() {
|
|
@@ -545,11 +536,10 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
545
536
|
|
|
546
537
|
/** A · B · C · D and where this figure stands, drawn from the bounds the grade is computed with. */
|
|
547
538
|
private renderGradeScale(now: string) {
|
|
548
|
-
const bounds = [...GRADE_SCALE.map(([grade, bound]) => [grade, `≤ ${bound * 100}%`]), ['D', '> 100%']]
|
|
549
539
|
return html`
|
|
550
540
|
<p scale>
|
|
551
541
|
<span>${i18next.t('figure.text.grade-scale-lead')}</span>
|
|
552
|
-
${
|
|
542
|
+
${gradeBoundsOf().map(([grade, bound]) => html`<span ?now=${grade === now}><b>${grade}</b> ${bound}</span>`)}
|
|
553
543
|
</p>
|
|
554
544
|
`
|
|
555
545
|
}
|
|
@@ -35,6 +35,29 @@ export function suggestTypeOf(name: string, taken: readonly string[], random: ()
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** The shape of an identifier the modeller offers (`suggestTypeOf`'s random form). */
|
|
39
|
+
const OFFERED = /^fig-[0-9a-z]{6}$/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The identifier the first save's question shows, given the one it opens on (Q-D3). The identifier is not the name:
|
|
43
|
+
* renaming does not change it, and a retried first save opens on the one answered before. A new one is offered only when
|
|
44
|
+
* there is none, or when an offered one is found taken -- told by the server, or in the list; one the author typed is
|
|
45
|
+
* kept as typed, and its refusal said. `suggested` is the identifier now on offer, absent when that is unchanged.
|
|
46
|
+
*/
|
|
47
|
+
export function offeredTypeOf(
|
|
48
|
+
type: string,
|
|
49
|
+
name: string,
|
|
50
|
+
taken: readonly string[],
|
|
51
|
+
random: () => number = Math.random
|
|
52
|
+
): { type: string; suggested?: string } {
|
|
53
|
+
const offeredAndTaken = OFFERED.test(type) && taken.includes(type)
|
|
54
|
+
if (!type || offeredAndTaken) {
|
|
55
|
+
const made = suggestTypeOf(name, taken, random)
|
|
56
|
+
return { type: made, suggested: made }
|
|
57
|
+
}
|
|
58
|
+
return OFFERED.test(type) ? { type, suggested: type } : { type }
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
/** Why the first save cannot go on with this type, as an i18n key, or null when it can. */
|
|
39
62
|
export function firstSaveRefusalOf(type: string, taken: readonly string[]): { key: string; type?: string } | null {
|
|
40
63
|
if (!type) return { key: 'figure.text.enter-a-type-name' }
|
package/client/modeller/links.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, figureFollowOf, proposeFigureProportion, defaultInstanceSizeRange, foldInstanceSizeRange, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeLimitsOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureFollow, FigureSizeRange } from '@hatiolab/figure-model'
|
|
3
|
-
import type { FigureAsset, FigureBindingPlace, FigureFace, FigureJoint } from '@hatiolab/figure-model'
|
|
3
|
+
import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureDimensionSource, FigureFace, FigureJoint } from '@hatiolab/figure-model'
|
|
4
4
|
import { startingShapeKindOf } from './starting-shape.js'
|
|
5
|
+
import { boxOf } from './align.js'
|
|
5
6
|
import { graphValuesOf, type FigureRevision } from './revision.js'
|
|
6
7
|
import { jointAxisToken, jointAxisVectorOf, type JointAxisVector } from './joint-axis.js'
|
|
7
8
|
|
|
@@ -178,6 +179,27 @@ export function dimensionNow(revision: FigureRevision, part: string, axis: Axis)
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The command a dimension's rule chosen in the size form sends (`choice` as `choiceOf` writes it), or nothing to send.
|
|
184
|
+
* The measure as it stands decides the numbers: a dimension stops following at it, and a share is taken of it, so
|
|
185
|
+
* neither choice changes the part's size. A share needs the figure's size on that axis.
|
|
186
|
+
*/
|
|
187
|
+
export function followCommandOf(revision: FigureRevision, part: string, axis: Axis, choice: string, now: Follow): FigureAuthoringAction | undefined {
|
|
188
|
+
const following = now.of !== 'own'
|
|
189
|
+
/* The part's own measure on the axis now (a cylinder's radius, not its width), else its extent as drawn. */
|
|
190
|
+
const current = dimensionNow(revision, part, axis) ?? boxOf(revision, part)?.dims[AXES.indexOf(axis)] ?? 100
|
|
191
|
+
if (choice === 'own') return following ? { kind: 'unlink-dimension', part, axis, mm: Math.max(1, Math.round(current)) } : undefined
|
|
192
|
+
const [of, a, b] = choice.split(':')
|
|
193
|
+
if (of === 'share') {
|
|
194
|
+
const size = revision.asset.designInputs[`size.${a}`]
|
|
195
|
+
if (!size) return undefined
|
|
196
|
+
const times = Math.round((current / size) * 1e6) / 1e6
|
|
197
|
+
return { kind: 'link-dimension', part, axis, source: { of: 'instance', axis: a as Axis }, times, replace: following }
|
|
198
|
+
}
|
|
199
|
+
const source: FigureDimensionSource = of === 'instance' ? { of: 'instance', axis: a as Axis } : { of: 'part', part: a!, axis: b as Axis }
|
|
200
|
+
return { kind: 'link-dimension', part, axis, source, replace: following }
|
|
201
|
+
}
|
|
202
|
+
|
|
181
203
|
/**
|
|
182
204
|
* How long a part is along one axis now, as it is drawn: the shape's own measure through figure-model's provider (a
|
|
183
205
|
* cylinder's width is twice its radius), read after any rule it follows -- so a linked measure still says its size.
|
|
@@ -290,6 +312,35 @@ export function alignAxesOf(asset: FigureAsset, part: string, target: { part: st
|
|
|
290
312
|
return AXES.filter(axis => axis !== along).map(axis => ({ axis, edges: !twisted && OUTER[axis].every(f => theirs.has(f) && mine.has(f)) }))
|
|
291
313
|
}
|
|
292
314
|
|
|
315
|
+
/**
|
|
316
|
+
* The faces of its own a part may be fastened by: its outer faces -- on the mounting plane only the ones the plane takes
|
|
317
|
+
* (a bottom or a top on the floor, a back on a wall) -- and, to another part, its centre, which a roller spins about.
|
|
318
|
+
*/
|
|
319
|
+
export function ownFacesOf(asset: FigureAsset, part: string, toPlane: boolean): Array<FigureFace | 'centre'> {
|
|
320
|
+
const outer = Object.keys(FIGURE_FACES) as FigureFace[]
|
|
321
|
+
const plane = mountingPlaneOf(asset)
|
|
322
|
+
return [
|
|
323
|
+
...(facesOf(asset, part).filter(face => outer.includes(face as FigureFace) && (!toPlane || plane.faces.includes(face as FigureFace))) as FigureFace[]),
|
|
324
|
+
...(toPlane ? [] : (['centre'] as const))
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The axes across the face the fastening form asks about in rows: only where the face drawing (`view`, its axes) cannot
|
|
330
|
+
* set the place -- no drawing at all, an axis it does not draw, a place that follows a position field, or one the
|
|
331
|
+
* figure's size may scale.
|
|
332
|
+
*/
|
|
333
|
+
export function alignRowsOf(
|
|
334
|
+
asset: FigureAsset,
|
|
335
|
+
part: string,
|
|
336
|
+
aim: { part: string; face: string } | 'plane',
|
|
337
|
+
view: { axes: readonly Axis[] } | undefined,
|
|
338
|
+
twisted: boolean,
|
|
339
|
+
alignInput: Partial<Record<Axis, string>> = {}
|
|
340
|
+
): { axis: Axis; edges: boolean }[] {
|
|
341
|
+
return alignAxesOf(asset, part, aim, twisted).filter(({ axis }) => !view || !view.axes.includes(axis) || !!alignInput[axis] || hasInstanceSize(asset, axis))
|
|
342
|
+
}
|
|
343
|
+
|
|
293
344
|
/** The controls a part's showing can follow: the figure's state inputs, with their range and label. */
|
|
294
345
|
export function visibilityControlsOf(asset: FigureAsset): Array<{ id: string; unit: string; min: number; max: number; label: string }> {
|
|
295
346
|
const labels = (asset as unknown as { stateInputs?: Record<string, { label?: string }> }).stateInputs ?? {}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FigureMaterial, FigurePartEdit } from '@hatiolab/figure-model'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The edit the material panel (`figure-material-panel`) sends for a change of a part's material: worked out here, apart
|
|
5
|
+
* from the panel, so what it sends can be read without a screen.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The colours a material is picked from: the app's palette, with the asset's own colours over it. */
|
|
9
|
+
export function offeredPaletteOf(app: Record<string, string>, asset: Record<string, string>): Record<string, string> {
|
|
10
|
+
return { ...app, ...asset }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The glow at an intensity: 0 is no glow at all; a new glow takes the part's own colour until another is picked. */
|
|
14
|
+
export function glowOf(material: FigureMaterial, intensity: number): Partial<FigureMaterial> {
|
|
15
|
+
return { emissive: intensity > 0 ? { token: material.emissive?.token ?? material.token, intensity, on: true } : undefined }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The material with a change over it, as one `set-material` edit. A field the change empties is left out -- the format
|
|
20
|
+
* refuses an undefined field. A colour it names that the asset does not carry yet (a new asset carries only its primary
|
|
21
|
+
* colour) is sent with it, from the colours offered, and added in the same edit.
|
|
22
|
+
*/
|
|
23
|
+
export function materialEditOf(
|
|
24
|
+
target: string,
|
|
25
|
+
material: FigureMaterial,
|
|
26
|
+
patch: Partial<FigureMaterial>,
|
|
27
|
+
assetPalette: Record<string, string>,
|
|
28
|
+
offered: Record<string, string>
|
|
29
|
+
): FigurePartEdit {
|
|
30
|
+
const next: FigureMaterial = { ...material, ...patch }
|
|
31
|
+
for (const key of Object.keys(next) as (keyof FigureMaterial)[]) if (next[key] === undefined) delete next[key]
|
|
32
|
+
const palette = Object.fromEntries(
|
|
33
|
+
[next.token, next.emissive?.token]
|
|
34
|
+
.filter((token): token is string => !!token && assetPalette[token] === undefined && offered[token] !== undefined)
|
|
35
|
+
.map(token => [token, offered[token]!])
|
|
36
|
+
)
|
|
37
|
+
return { kind: 'set-material', target, material: next, ...(Object.keys(palette).length ? { palette } : {}) }
|
|
38
|
+
}
|
|
@@ -50,6 +50,11 @@ import type { FigureCapacity } from './scene-budget.js'
|
|
|
50
50
|
export { FIGURE_BUDGET_KEYS, FIGURE_GRADE_SCALE as GRADE_SCALE, figureGradeOf as gradeOf }
|
|
51
51
|
export type { FigureBudgetKey, FigureBudgetUse, FigureScore }
|
|
52
52
|
|
|
53
|
+
/** A · B · C · D as the tab draws its scale: each grade with the bound it is computed with, and D past the last. */
|
|
54
|
+
export function gradeBoundsOf(): [string, string][] {
|
|
55
|
+
return [...FIGURE_GRADE_SCALE.map(([grade, bound]): [string, string] => [grade, `≤ ${bound * 100}%`]), ['D', '> 100%']]
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
export type FigureAxisKey = 'scale' | 'consistency' | 'reuse' | 'detail' | 'reactive' | 'readable'
|
|
54
59
|
|
|
55
60
|
/** One maturity axis. */
|
|
@@ -411,6 +416,29 @@ export interface FigureBudgetLine {
|
|
|
411
416
|
tightest: { key: string; used: number; limit: number }
|
|
412
417
|
}
|
|
413
418
|
|
|
419
|
+
/**
|
|
420
|
+
* What the verdict tab measures of the figure on screen once it settles: the report, the bodies there are to fit the
|
|
421
|
+
* space to (drawn or hidden -- a hidden part is still a part to measure), and what it tells the page: the count for its
|
|
422
|
+
* tab and the parts list's line, from the same report. A source the format refuses has no report.
|
|
423
|
+
*/
|
|
424
|
+
export function verdictMeasureOf(asset: FigureAsset | undefined): {
|
|
425
|
+
report?: FigureReport
|
|
426
|
+
bodies: number
|
|
427
|
+
told: { instances?: number; budget?: FigureBudgetLine }
|
|
428
|
+
} {
|
|
429
|
+
let report: FigureReport | undefined
|
|
430
|
+
let bodies = 0
|
|
431
|
+
try {
|
|
432
|
+
const cost = asset ? figureCostOf(asset) : undefined
|
|
433
|
+
bodies = cost ? cost.groups + cost.hidden : 0
|
|
434
|
+
report = asset ? figureReportOf(asset) : undefined
|
|
435
|
+
} catch {
|
|
436
|
+
/* A source the format refuses has no report; the server's finding says why. */
|
|
437
|
+
report = undefined
|
|
438
|
+
}
|
|
439
|
+
return { report, bodies, told: { instances: report?.capacity.instances, budget: budgetOf(report) } }
|
|
440
|
+
}
|
|
441
|
+
|
|
414
442
|
export function budgetOf(report: FigureReport | undefined): FigureBudgetLine | undefined {
|
|
415
443
|
if (!report) return undefined
|
|
416
444
|
const { tightest } = report.score
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A GLB made into a mesh part (ADR-0129, figure-model contract §13). The server reads the file when it is uploaded and
|
|
3
|
+
* answers its hash and what it holds; this turns that answer and the author's choice -- the whole file, or one node of it
|
|
4
|
+
* -- into the part figure-model adds. Kept free of Lit and the network so node tests read it directly.
|
|
5
|
+
*/
|
|
6
|
+
import type { FigurePrimitiveCreation } from '@hatiolab/figure-model'
|
|
7
|
+
|
|
8
|
+
type Vec = { x: number; y: number; z: number }
|
|
9
|
+
type Box = { min: Vec; max: Vec }
|
|
10
|
+
|
|
11
|
+
/** What `uploadFigureMesh` answers: the file's hash and what the server read of it. */
|
|
12
|
+
export interface MeshUpload {
|
|
13
|
+
hash: string
|
|
14
|
+
summary: {
|
|
15
|
+
bounds: Box
|
|
16
|
+
triangles: number
|
|
17
|
+
primitives: number
|
|
18
|
+
unit: 'm'
|
|
19
|
+
nodes: Array<{
|
|
20
|
+
index: number
|
|
21
|
+
name: string
|
|
22
|
+
parent: number | null
|
|
23
|
+
bounds: Box | null
|
|
24
|
+
triangles: number
|
|
25
|
+
primitives: number
|
|
26
|
+
position: Vec
|
|
27
|
+
rotation: Vec
|
|
28
|
+
refused?: 'no-mesh' | 'sheared'
|
|
29
|
+
}>
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the author can make a part of: the whole file first, then each node that holds a mesh and has a frame. */
|
|
34
|
+
export function meshChoicesOf(upload: MeshUpload): Array<{ node?: number; name: string; triangles: number }> {
|
|
35
|
+
return [
|
|
36
|
+
{ name: '', triangles: upload.summary.triangles },
|
|
37
|
+
...upload.summary.nodes.filter(n => n.bounds && !n.refused).map(n => ({ node: n.index, name: n.name, triangles: n.triangles }))
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A part id from a node's name: letter-led, letters, digits, `_` and `-`; another number when it is taken. */
|
|
42
|
+
export function meshPartIdOf(name: string, taken: ReadonlySet<string>): string {
|
|
43
|
+
const cleaned = name.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^[^a-zA-Z]+/, '').replace(/-+$/, '')
|
|
44
|
+
const base = cleaned || 'mesh'
|
|
45
|
+
if (!taken.has(base)) return base
|
|
46
|
+
let n = 2
|
|
47
|
+
while (taken.has(`${base}-${n}`)) n++
|
|
48
|
+
return `${base}-${n}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The part: its bounds and counts as the server read them, its size the reach along its longest axis (so it is 1:1),
|
|
53
|
+
* and -- for a node -- standing where the node stood in the file, so a crane taken apart node by node stands as it did.
|
|
54
|
+
*/
|
|
55
|
+
export function meshPartOf(upload: MeshUpload, choice: { node?: number }, id: string): FigurePrimitiveCreation {
|
|
56
|
+
const node = choice.node === undefined ? undefined : upload.summary.nodes.find(n => n.index === choice.node)
|
|
57
|
+
if (choice.node !== undefined && (!node?.bounds || node.refused)) throw new Error(`node ${choice.node} of the file cannot be a part`)
|
|
58
|
+
const bounds = node ? node.bounds! : upload.summary.bounds
|
|
59
|
+
const size = Math.max(bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y, bounds.max.z - bounds.min.z)
|
|
60
|
+
const turned = node && (node.rotation.x || node.rotation.y || node.rotation.z)
|
|
61
|
+
return {
|
|
62
|
+
kind: 'mesh',
|
|
63
|
+
id,
|
|
64
|
+
dimensions: { size },
|
|
65
|
+
mesh: {
|
|
66
|
+
asset: upload.hash,
|
|
67
|
+
format: 'glb',
|
|
68
|
+
unit: upload.summary.unit,
|
|
69
|
+
...(node ? { node: node.index } : {}),
|
|
70
|
+
bounds,
|
|
71
|
+
triangles: node ? node.triangles : upload.summary.triangles,
|
|
72
|
+
primitives: node ? node.primitives : upload.summary.primitives
|
|
73
|
+
},
|
|
74
|
+
position: node ? { ...node.position } : { x: 0, y: 0, z: 0 },
|
|
75
|
+
...(turned ? { rotation: { ...node!.rotation } } : {})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -7,3 +7,12 @@ export const MODELLER = 'figure-modeller'
|
|
|
7
7
|
export function modellerPathOf(figure?: { id?: string }): string {
|
|
8
8
|
return figure?.id ? `${MODELLER}/${figure.id}` : MODELLER
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where one released version downloads as a package file, by its release reference {namespace}/{type}@{version}
|
|
13
|
+
* (ADR-0114) -- or undefined before the figure has a full name.
|
|
14
|
+
*/
|
|
15
|
+
export function packageHrefOf(contextPath: string, namespace: string | null | undefined, type: string | null | undefined, version: number): string | undefined {
|
|
16
|
+
if (!namespace || !type) return undefined
|
|
17
|
+
return `${contextPath}/figure-package?refs=${encodeURIComponent(`${namespace}/${type}@${version}`)}`
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FigureFace } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureFigureFacts } from './figure-facts.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The mounting face the properties tab offers: the part and face picked in its lists, else the one set, else where the
|
|
6
|
+
* figure meets its plane (`suggested`) -- a conveyor's legs, not its bed (walk 1, 2026-09-26) -- else the first part.
|
|
7
|
+
* A part no longer in the figure is passed over.
|
|
8
|
+
*/
|
|
9
|
+
export function offeredMountingOf(
|
|
10
|
+
facts: Pick<FigureFigureFacts, 'parts' | 'mounting' | 'suggested'>,
|
|
11
|
+
picked: { part?: string; face?: FigureFace } = {}
|
|
12
|
+
): { part?: string; face: FigureFace } {
|
|
13
|
+
const part = [picked.part, facts.mounting?.part, facts.suggested?.part, facts.parts[0]].find(one => one && facts.parts.includes(one))
|
|
14
|
+
return { part, face: picked.face ?? facts.mounting?.face ?? facts.suggested?.face ?? 'bottom' }
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** What came back for a question: its answer, or why there is none. */
|
|
2
|
+
export type FigureOutcome<T> = { answer: T } | { failure: unknown }
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Questions asked again while an earlier one is still out (a figure edited while the server judges it): answers can
|
|
6
|
+
* come back out of order, and only the newest question's lands -- its answer or its failure alike. An older one arriving
|
|
7
|
+
* late is dropped, so it never covers the newer one.
|
|
8
|
+
*/
|
|
9
|
+
export function newestAnswer<T>() {
|
|
10
|
+
let asked = 0
|
|
11
|
+
return async (question: () => Promise<T>, land: (outcome: FigureOutcome<T>) => void): Promise<void> => {
|
|
12
|
+
const mine = ++asked
|
|
13
|
+
let outcome: FigureOutcome<T>
|
|
14
|
+
try {
|
|
15
|
+
outcome = { answer: await question() }
|
|
16
|
+
} catch (failure) {
|
|
17
|
+
outcome = { failure }
|
|
18
|
+
}
|
|
19
|
+
if (mine === asked) land(outcome)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -5,6 +5,11 @@ import type { FigureAsset } from '@hatiolab/figure-model'
|
|
|
5
5
|
|
|
6
6
|
export const PREVIEW_FACTOR = 2
|
|
7
7
|
|
|
8
|
+
/** The type the preview registers the figure under: this figure's own, never a real type a board uses. */
|
|
9
|
+
export function previewTypeOf(context: string): string {
|
|
10
|
+
return `figure-preview-${context || 'new'}`
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' | 'z')[] }[] = [
|
|
9
14
|
{ name: 'base', axes: [] },
|
|
10
15
|
{ name: 'x', axes: ['x'] },
|