@things-factory/figure-ui 10.1.39 → 10.1.42
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/graphql/index.ts +1 -0
- package/client/modeller/figure-v3-material.ts +248 -0
- package/client/modeller/figure-v3-parts.ts +16 -1
- package/client/modeller/figure-v3-properties.ts +254 -0
- package/client/modeller/modeller-route.ts +25 -0
- package/client/modeller/v3-align.ts +84 -0
- package/client/modeller/v3-canvas.ts +167 -5
- package/client/modeller/v3-editor.ts +207 -47
- package/client/modeller/v3-figure-facts.ts +53 -0
- package/client/modeller/v3-gizmo.ts +80 -0
- package/client/modeller/v3-occupancy.ts +6 -2
- package/client/pages/figure-list-page.ts +5 -3
- package/client/pages/figure-modeller-page.ts +9 -0
- package/client/pages/figure-v3-modeller-page.ts +68 -13
- package/client/types.ts +2 -0
- package/dist-client/graphql/index.js +1 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +36 -0
- package/dist-client/modeller/figure-v3-material.js +249 -0
- package/dist-client/modeller/figure-v3-material.js.map +1 -0
- package/dist-client/modeller/figure-v3-parts.js +16 -1
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +46 -0
- package/dist-client/modeller/figure-v3-properties.js +259 -0
- package/dist-client/modeller/figure-v3-properties.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +16 -0
- package/dist-client/modeller/modeller-route.js +27 -0
- package/dist-client/modeller/modeller-route.js.map +1 -0
- package/dist-client/modeller/v3-align.d.ts +26 -0
- package/dist-client/modeller/v3-align.js +65 -0
- package/dist-client/modeller/v3-align.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +24 -0
- package/dist-client/modeller/v3-canvas.js +181 -5
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-editor.d.ts +24 -0
- package/dist-client/modeller/v3-editor.js +218 -44
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-figure-facts.d.ts +26 -0
- package/dist-client/modeller/v3-figure-facts.js +34 -0
- package/dist-client/modeller/v3-figure-facts.js.map +1 -0
- package/dist-client/modeller/v3-gizmo.d.ts +48 -0
- package/dist-client/modeller/v3-gizmo.js +40 -0
- package/dist-client/modeller/v3-gizmo.js.map +1 -0
- package/dist-client/modeller/v3-occupancy.d.ts +5 -1
- package/dist-client/modeller/v3-occupancy.js +4 -2
- package/dist-client/modeller/v3-occupancy.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +1 -0
- package/dist-client/pages/figure-list-page.js +5 -3
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.js +9 -0
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +16 -2
- package/dist-client/pages/figure-v3-modeller-page.js +78 -22
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +4 -4
- package/test/modeller-route.test.ts +42 -0
- package/test/v3-align.test.ts +64 -0
- package/test/v3-gizmo.test.ts +97 -0
- package/test/v3-material.test.ts +68 -0
- package/test/v3-parts-wiring.test.ts +56 -0
- package/test/v3-properties.test.ts +93 -0
- package/test/v3-theme.test.ts +67 -0
- package/translations/en.json +11 -0
- package/translations/ja.json +11 -0
- package/translations/ko.json +11 -0
- package/translations/ms.json +11 -0
- package/translations/zh.json +11 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
|
+
import { i18next, localize } from '@operato/i18n';
|
|
6
|
+
import { V3_FACES } from '@hatiolab/figure-model';
|
|
7
|
+
import { occupancyFromParts } from './v3-occupancy.js';
|
|
8
|
+
import { tagsOf, v3FigureFacts } from './v3-figure-facts.js';
|
|
9
|
+
const PLACEMENTS = ['floor', 'ceiling', 'center'];
|
|
10
|
+
const FACES = Object.keys(V3_FACES);
|
|
11
|
+
/**
|
|
12
|
+
* 속성 — the V3 modeller's figure properties (parity row ②).
|
|
13
|
+
*
|
|
14
|
+
* The V2 inspector opens with what the figure is: its name, the face it is placed by, the room it takes up, where
|
|
15
|
+
* the catalogue files it. The V3 page had only the type. The same rows, in the same order, drawn the way the V2
|
|
16
|
+
* inspector draws them.
|
|
17
|
+
*
|
|
18
|
+
* Two kinds of change leave this tab, and it does neither itself:
|
|
19
|
+
* - `v3-command` carries an authoring command (rename, declare the room, the mounting face). The page runs it
|
|
20
|
+
* through the session, so it is one step of the same undo history as every other edit, and is saved with it.
|
|
21
|
+
* - `v3-catalog` carries a change to the row (category, tags). Those are not in the asset; the page writes them
|
|
22
|
+
* to the saved row at once, so they need a saved figure.
|
|
23
|
+
*
|
|
24
|
+
* Not here yet, because V3 has no place for them: editing the declared room by hand (V3 declares it from the
|
|
25
|
+
* parts, as graph expressions), and the detail level (the converter records it as lost). Both wait on a V3 ruling.
|
|
26
|
+
*/
|
|
27
|
+
let FigureV3Properties = class FigureV3Properties extends localize(i18next)(LitElement) {
|
|
28
|
+
static { this.styles = css `
|
|
29
|
+
:host {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
gap: 12px;
|
|
33
|
+
padding: 12px;
|
|
34
|
+
overflow: auto;
|
|
35
|
+
font-size: 0.76rem;
|
|
36
|
+
color: var(--md-sys-color-on-surface);
|
|
37
|
+
}
|
|
38
|
+
div[field] {
|
|
39
|
+
display: grid;
|
|
40
|
+
gap: 4px;
|
|
41
|
+
}
|
|
42
|
+
div[field] > label {
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
}
|
|
45
|
+
div[row] {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 6px;
|
|
48
|
+
align-items: center;
|
|
49
|
+
}
|
|
50
|
+
input,
|
|
51
|
+
select,
|
|
52
|
+
button {
|
|
53
|
+
font: inherit;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
}
|
|
56
|
+
input,
|
|
57
|
+
select {
|
|
58
|
+
padding: 5px 8px;
|
|
59
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
background: var(--md-sys-color-surface);
|
|
62
|
+
color: var(--md-sys-color-on-surface);
|
|
63
|
+
}
|
|
64
|
+
div[row] > select {
|
|
65
|
+
flex: 1;
|
|
66
|
+
}
|
|
67
|
+
button {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 4px;
|
|
71
|
+
padding: 4px 10px;
|
|
72
|
+
border: none;
|
|
73
|
+
border-radius: 999em;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
76
|
+
background: var(--md-sys-color-secondary-container);
|
|
77
|
+
}
|
|
78
|
+
button:disabled,
|
|
79
|
+
input:disabled,
|
|
80
|
+
select:disabled {
|
|
81
|
+
opacity: 0.5;
|
|
82
|
+
cursor: default;
|
|
83
|
+
}
|
|
84
|
+
button md-icon {
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
width: 16px;
|
|
87
|
+
height: 16px;
|
|
88
|
+
}
|
|
89
|
+
small {
|
|
90
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
91
|
+
}
|
|
92
|
+
`; }
|
|
93
|
+
facts() {
|
|
94
|
+
try {
|
|
95
|
+
return this.source ? v3FigureFacts(this.source) : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* A source the format refuses has no facts to show; the verdict tab says why. */
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
render() {
|
|
103
|
+
const facts = this.facts();
|
|
104
|
+
if (!facts)
|
|
105
|
+
return nothing;
|
|
106
|
+
return html `${this.renderName(facts)} ${this.renderPlacement(facts)} ${this.renderSpace(facts)} ${this.renderMounting(facts)}
|
|
107
|
+
${this.renderCatalog()}`;
|
|
108
|
+
}
|
|
109
|
+
renderName(facts) {
|
|
110
|
+
return html `
|
|
111
|
+
<div field>
|
|
112
|
+
<label>${i18next.t('figure.label.name')}</label>
|
|
113
|
+
<input
|
|
114
|
+
.value=${facts.name}
|
|
115
|
+
placeholder=${i18next.t('figure.text.figure-display-name')}
|
|
116
|
+
@change=${(event) => {
|
|
117
|
+
const input = event.target;
|
|
118
|
+
if (!input.value.trim()) {
|
|
119
|
+
/* The format refuses a blank name; put the old one back rather than send a command that fails. */
|
|
120
|
+
input.value = facts.name;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (input.value.trim() !== facts.name)
|
|
124
|
+
this.command({ kind: 'rename', name: input.value });
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The face the figure is placed by. In V3 it is part of the room declaration, so choosing one declares the room
|
|
132
|
+
* again from the parts — the only way V3 declares it — with the new face. The hint says so.
|
|
133
|
+
*/
|
|
134
|
+
renderPlacement(facts) {
|
|
135
|
+
const current = facts.placement ?? 'floor';
|
|
136
|
+
return html `
|
|
137
|
+
<div field>
|
|
138
|
+
<label>${i18next.t('figure.label.figure-placement')}</label>
|
|
139
|
+
<select
|
|
140
|
+
?disabled=${!facts.parts.length}
|
|
141
|
+
@change=${(event) => this.declare(event.target.value)}
|
|
142
|
+
>
|
|
143
|
+
${PLACEMENTS.map(one => html `<option value=${one} ?selected=${one === current}>${i18next.t('figure.label.placement-' + one)}</option>`)}
|
|
144
|
+
</select>
|
|
145
|
+
<small>${i18next.t(facts.parts.length ? 'figure.text.placement-redeclares' : 'figure.text.fit-needs-parts')}</small>
|
|
146
|
+
</div>
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
renderSpace(facts) {
|
|
150
|
+
return html `
|
|
151
|
+
<div field>
|
|
152
|
+
<label>${i18next.t('figure.label.declared-space')}</label>
|
|
153
|
+
<div row>
|
|
154
|
+
<span>${i18next.t(facts.declared ? 'figure.text.declared-space-declared' : 'figure.text.declared-space-none')}</span>
|
|
155
|
+
<button fit ?disabled=${!facts.parts.length} @click=${() => this.declare(facts.placement ?? 'floor')}>
|
|
156
|
+
<md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
renderMounting(facts) {
|
|
163
|
+
const mounting = facts.mounting;
|
|
164
|
+
const part = [this.pickedPart, mounting?.part, facts.parts[0]].find(one => one && facts.parts.includes(one));
|
|
165
|
+
const face = this.pickedFace ?? mounting?.face ?? 'bottom';
|
|
166
|
+
return html `
|
|
167
|
+
<div field>
|
|
168
|
+
<label>${i18next.t('figure.label.mounting-face')}</label>
|
|
169
|
+
<div row>
|
|
170
|
+
<select ?disabled=${!facts.declared} @change=${(e) => (this.pickedPart = e.target.value)}>
|
|
171
|
+
${facts.parts.map(one => html `<option value=${one} ?selected=${one === part}>${one}</option>`)}
|
|
172
|
+
</select>
|
|
173
|
+
<select ?disabled=${!facts.declared} @change=${(e) => (this.pickedFace = e.target.value)}>
|
|
174
|
+
${FACES.map(one => html `<option value=${one} ?selected=${one === face}>${i18next.t('figure.label.face-' + one)}</option>`)}
|
|
175
|
+
</select>
|
|
176
|
+
</div>
|
|
177
|
+
<div row>
|
|
178
|
+
<button ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part, face })}>
|
|
179
|
+
${i18next.t('figure.button.set-mounting-face')}
|
|
180
|
+
</button>
|
|
181
|
+
<button ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>
|
|
182
|
+
${i18next.t('figure.button.clear-mounting-face')}
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
<small>
|
|
186
|
+
${facts.declared
|
|
187
|
+
? mounting
|
|
188
|
+
? `${mounting.part} · ${i18next.t('figure.label.face-' + mounting.face)}`
|
|
189
|
+
: i18next.t('figure.label.mounting-face-none')
|
|
190
|
+
: i18next.t('figure.text.mounting-face-needs-space')}
|
|
191
|
+
</small>
|
|
192
|
+
</div>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
renderCatalog() {
|
|
196
|
+
const saved = !!this.figureId;
|
|
197
|
+
return html `
|
|
198
|
+
<div field>
|
|
199
|
+
<label>${i18next.t('figure.label.category')}</label>
|
|
200
|
+
<input
|
|
201
|
+
.value=${this.figureCategory ?? ''}
|
|
202
|
+
?disabled=${!saved}
|
|
203
|
+
placeholder=${i18next.t('figure.text.category-placeholder')}
|
|
204
|
+
@change=${(e) => this.catalog({ category: e.target.value.trim() })}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
<div field>
|
|
208
|
+
<label>${i18next.t('figure.label.tags')}</label>
|
|
209
|
+
<input
|
|
210
|
+
.value=${(this.figureTags ?? []).join(', ')}
|
|
211
|
+
?disabled=${!saved}
|
|
212
|
+
placeholder=${i18next.t('figure.text.tags-placeholder')}
|
|
213
|
+
@change=${(e) => this.catalog({ tags: tagsOf(e.target.value) })}
|
|
214
|
+
/>
|
|
215
|
+
${saved ? nothing : html `<small>${i18next.t('figure.text.catalog-needs-a-save')}</small>`}
|
|
216
|
+
</div>
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
declare(placement) {
|
|
220
|
+
if (!this.source)
|
|
221
|
+
return;
|
|
222
|
+
this.command(occupancyFromParts(this.source, placement));
|
|
223
|
+
}
|
|
224
|
+
command(action) {
|
|
225
|
+
this.dispatchEvent(new CustomEvent('v3-command', { detail: { action }, bubbles: true, composed: true }));
|
|
226
|
+
}
|
|
227
|
+
catalog(patch) {
|
|
228
|
+
this.dispatchEvent(new CustomEvent('v3-catalog', { detail: patch, bubbles: true, composed: true }));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
__decorate([
|
|
232
|
+
property({ type: String }),
|
|
233
|
+
__metadata("design:type", String)
|
|
234
|
+
], FigureV3Properties.prototype, "source", void 0);
|
|
235
|
+
__decorate([
|
|
236
|
+
property({ type: String }),
|
|
237
|
+
__metadata("design:type", String)
|
|
238
|
+
], FigureV3Properties.prototype, "figureId", void 0);
|
|
239
|
+
__decorate([
|
|
240
|
+
property({ type: String }),
|
|
241
|
+
__metadata("design:type", String)
|
|
242
|
+
], FigureV3Properties.prototype, "figureCategory", void 0);
|
|
243
|
+
__decorate([
|
|
244
|
+
property({ type: Array }),
|
|
245
|
+
__metadata("design:type", Array)
|
|
246
|
+
], FigureV3Properties.prototype, "figureTags", void 0);
|
|
247
|
+
__decorate([
|
|
248
|
+
state(),
|
|
249
|
+
__metadata("design:type", String)
|
|
250
|
+
], FigureV3Properties.prototype, "pickedPart", void 0);
|
|
251
|
+
__decorate([
|
|
252
|
+
state(),
|
|
253
|
+
__metadata("design:type", String)
|
|
254
|
+
], FigureV3Properties.prototype, "pickedFace", void 0);
|
|
255
|
+
FigureV3Properties = __decorate([
|
|
256
|
+
customElement('figure-v3-properties')
|
|
257
|
+
], FigureV3Properties);
|
|
258
|
+
export { FigureV3Properties };
|
|
259
|
+
//# sourceMappingURL=figure-v3-properties.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-v3-properties.js","sourceRoot":"","sources":["../../client/modeller/figure-v3-properties.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { V3_FACES } from '@hatiolab/figure-model'\nimport type { V3AuthoringAction, V3Face } from '@hatiolab/figure-model'\n\nimport { occupancyFromParts } from './v3-occupancy.js'\nimport { tagsOf, v3FigureFacts, type Placement, type V3FigureFacts } from './v3-figure-facts.js'\n\nconst PLACEMENTS: Placement[] = ['floor', 'ceiling', 'center']\nconst FACES = Object.keys(V3_FACES) as V3Face[]\n\n/**\n * 속성 — the V3 modeller's figure properties (parity row ②).\n *\n * The V2 inspector opens with what the figure is: its name, the face it is placed by, the room it takes up, where\n * the catalogue files it. The V3 page had only the type. The same rows, in the same order, drawn the way the V2\n * inspector draws them.\n *\n * Two kinds of change leave this tab, and it does neither itself:\n * - `v3-command` carries an authoring command (rename, declare the room, the mounting face). The page runs it\n * through the session, so it is one step of the same undo history as every other edit, and is saved with it.\n * - `v3-catalog` carries a change to the row (category, tags). Those are not in the asset; the page writes them\n * to the saved row at once, so they need a saved figure.\n *\n * Not here yet, because V3 has no place for them: editing the declared room by hand (V3 declares it from the\n * parts, as graph expressions), and the detail level (the converter records it as lost). Both wait on a V3 ruling.\n */\n@customElement('figure-v3-properties')\nexport class FigureV3Properties extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 12px;\n overflow: auto;\n font-size: 0.76rem;\n color: var(--md-sys-color-on-surface);\n }\n div[field] {\n display: grid;\n gap: 4px;\n }\n div[field] > label {\n font-weight: 600;\n }\n div[row] {\n display: flex;\n gap: 6px;\n align-items: center;\n }\n input,\n select,\n button {\n font: inherit;\n min-width: 0;\n }\n input,\n select {\n padding: 5px 8px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 4px;\n background: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n div[row] > select {\n flex: 1;\n }\n button {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 4px 10px;\n border: none;\n border-radius: 999em;\n cursor: pointer;\n color: var(--md-sys-color-on-secondary-container);\n background: var(--md-sys-color-secondary-container);\n }\n button:disabled,\n input:disabled,\n select:disabled {\n opacity: 0.5;\n cursor: default;\n }\n button md-icon {\n font-size: 16px;\n width: 16px;\n height: 16px;\n }\n small {\n color: var(--md-sys-color-on-surface-variant);\n }\n `\n\n /** The source being edited. */\n @property({ type: String }) source?: string\n /** The saved row, if any. Category and tags live on it. */\n @property({ type: String }) figureId?: string\n @property({ type: String }) figureCategory?: string\n @property({ type: Array }) figureTags?: string[]\n /** The part and face picked for the mounting face, before 「이 면으로」 is pressed. */\n @state() private pickedPart?: string\n @state() private pickedFace?: V3Face\n\n private facts(): V3FigureFacts | undefined {\n try {\n return this.source ? v3FigureFacts(this.source) : undefined\n } catch {\n /* A source the format refuses has no facts to show; the verdict tab says why. */\n return undefined\n }\n }\n\n render() {\n const facts = this.facts()\n if (!facts) return nothing\n return html`${this.renderName(facts)} ${this.renderPlacement(facts)} ${this.renderSpace(facts)} ${this.renderMounting(facts)}\n ${this.renderCatalog()}`\n }\n\n private renderName(facts: V3FigureFacts) {\n return html`\n <div field>\n <label>${i18next.t('figure.label.name')}</label>\n <input\n .value=${facts.name}\n placeholder=${i18next.t('figure.text.figure-display-name')}\n @change=${(event: Event) => {\n const input = event.target as HTMLInputElement\n if (!input.value.trim()) {\n /* The format refuses a blank name; put the old one back rather than send a command that fails. */\n input.value = facts.name\n return\n }\n if (input.value.trim() !== facts.name) this.command({ kind: 'rename', name: input.value })\n }}\n />\n </div>\n `\n }\n\n /**\n * The face the figure is placed by. In V3 it is part of the room declaration, so choosing one declares the room\n * again from the parts — the only way V3 declares it — with the new face. The hint says so.\n */\n private renderPlacement(facts: V3FigureFacts) {\n const current = facts.placement ?? 'floor'\n return html`\n <div field>\n <label>${i18next.t('figure.label.figure-placement')}</label>\n <select\n ?disabled=${!facts.parts.length}\n @change=${(event: Event) => this.declare((event.target as HTMLSelectElement).value as Placement)}\n >\n ${PLACEMENTS.map(\n one => html`<option value=${one} ?selected=${one === current}>${i18next.t('figure.label.placement-' + one)}</option>`\n )}\n </select>\n <small>${i18next.t(facts.parts.length ? 'figure.text.placement-redeclares' : 'figure.text.fit-needs-parts')}</small>\n </div>\n `\n }\n\n private renderSpace(facts: V3FigureFacts) {\n return html`\n <div field>\n <label>${i18next.t('figure.label.declared-space')}</label>\n <div row>\n <span>${i18next.t(facts.declared ? 'figure.text.declared-space-declared' : 'figure.text.declared-space-none')}</span>\n <button fit ?disabled=${!facts.parts.length} @click=${() => this.declare(facts.placement ?? 'floor')}>\n <md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}\n </button>\n </div>\n </div>\n `\n }\n\n private renderMounting(facts: V3FigureFacts) {\n const mounting = facts.mounting\n const part = [this.pickedPart, mounting?.part, facts.parts[0]].find(one => one && facts.parts.includes(one))\n const face: V3Face = this.pickedFace ?? mounting?.face ?? 'bottom'\n return html`\n <div field>\n <label>${i18next.t('figure.label.mounting-face')}</label>\n <div row>\n <select ?disabled=${!facts.declared} @change=${(e: Event) => (this.pickedPart = (e.target as HTMLSelectElement).value)}>\n ${facts.parts.map(one => html`<option value=${one} ?selected=${one === part}>${one}</option>`)}\n </select>\n <select ?disabled=${!facts.declared} @change=${(e: Event) => (this.pickedFace = (e.target as HTMLSelectElement).value as V3Face)}>\n ${FACES.map(one => html`<option value=${one} ?selected=${one === face}>${i18next.t('figure.label.face-' + one)}</option>`)}\n </select>\n </div>\n <div row>\n <button ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part!, face })}>\n ${i18next.t('figure.button.set-mounting-face')}\n </button>\n <button ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>\n ${i18next.t('figure.button.clear-mounting-face')}\n </button>\n </div>\n <small>\n ${facts.declared\n ? mounting\n ? `${mounting.part} · ${i18next.t('figure.label.face-' + mounting.face)}`\n : i18next.t('figure.label.mounting-face-none')\n : i18next.t('figure.text.mounting-face-needs-space')}\n </small>\n </div>\n `\n }\n\n private renderCatalog() {\n const saved = !!this.figureId\n return html`\n <div field>\n <label>${i18next.t('figure.label.category')}</label>\n <input\n .value=${this.figureCategory ?? ''}\n ?disabled=${!saved}\n placeholder=${i18next.t('figure.text.category-placeholder')}\n @change=${(e: Event) => this.catalog({ category: (e.target as HTMLInputElement).value.trim() })}\n />\n </div>\n <div field>\n <label>${i18next.t('figure.label.tags')}</label>\n <input\n .value=${(this.figureTags ?? []).join(', ')}\n ?disabled=${!saved}\n placeholder=${i18next.t('figure.text.tags-placeholder')}\n @change=${(e: Event) => this.catalog({ tags: tagsOf((e.target as HTMLInputElement).value) })}\n />\n ${saved ? nothing : html`<small>${i18next.t('figure.text.catalog-needs-a-save')}</small>`}\n </div>\n `\n }\n\n private declare(placement: Placement) {\n if (!this.source) return\n this.command(occupancyFromParts(this.source, placement))\n }\n\n private command(action: V3AuthoringAction) {\n this.dispatchEvent(new CustomEvent('v3-command', { detail: { action }, bubbles: true, composed: true }))\n }\n\n private catalog(patch: { category?: string; tags?: string[] }) {\n this.dispatchEvent(new CustomEvent('v3-catalog', { detail: patch, bubbles: true, composed: true }))\n }\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which modeller opens a figure — by the format of its source, which the server reports (`format`).
|
|
3
|
+
*
|
|
4
|
+
* The catalogue sent every figure to the V2 modeller, so a converted figure could only be opened by typing the V3
|
|
5
|
+
* page's address (2026-09-24). A new figure starts in V3: new V2 figures would only add to what the cutover has to
|
|
6
|
+
* convert (ADR-0087). A figure is opened in V3 only when the server says it is V3 — an unknown format goes to the
|
|
7
|
+
* V2 page, which reports what it could not read.
|
|
8
|
+
*/
|
|
9
|
+
export declare const V3_MODELLER = "figure-v3-modeller";
|
|
10
|
+
export declare const V2_MODELLER = "figure-modeller";
|
|
11
|
+
export declare function modellerPathOf(figure?: {
|
|
12
|
+
id?: string;
|
|
13
|
+
format?: string | null;
|
|
14
|
+
}): string;
|
|
15
|
+
/** The version a V3 asset stamps on itself. The V2 page forwards such a figure rather than failing on it. */
|
|
16
|
+
export declare function isV3Source(source?: string | null): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which modeller opens a figure — by the format of its source, which the server reports (`format`).
|
|
3
|
+
*
|
|
4
|
+
* The catalogue sent every figure to the V2 modeller, so a converted figure could only be opened by typing the V3
|
|
5
|
+
* page's address (2026-09-24). A new figure starts in V3: new V2 figures would only add to what the cutover has to
|
|
6
|
+
* convert (ADR-0087). A figure is opened in V3 only when the server says it is V3 — an unknown format goes to the
|
|
7
|
+
* V2 page, which reports what it could not read.
|
|
8
|
+
*/
|
|
9
|
+
export const V3_MODELLER = 'figure-v3-modeller';
|
|
10
|
+
export const V2_MODELLER = 'figure-modeller';
|
|
11
|
+
export function modellerPathOf(figure) {
|
|
12
|
+
if (!figure?.id)
|
|
13
|
+
return V3_MODELLER;
|
|
14
|
+
return figure.format === 'v3' ? `${V3_MODELLER}/${figure.id}` : `${V2_MODELLER}/${figure.id}`;
|
|
15
|
+
}
|
|
16
|
+
/** The version a V3 asset stamps on itself. The V2 page forwards such a figure rather than failing on it. */
|
|
17
|
+
export function isV3Source(source) {
|
|
18
|
+
if (!source)
|
|
19
|
+
return false;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(source)?.version === 'figure-v3-asset-1';
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=modeller-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modeller-route.js","sourceRoot":"","sources":["../../client/modeller/modeller-route.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,oBAAoB,CAAA;AAC/C,MAAM,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAA;AAE5C,MAAM,UAAU,cAAc,CAAC,MAAgD;IAC7E,IAAI,CAAC,MAAM,EAAE,EAAE;QAAE,OAAO,WAAW,CAAA;IACnC,OAAO,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,MAAM,CAAC,EAAE,EAAE,CAAA;AAC/F,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,UAAU,CAAC,MAAsB;IAC/C,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,IAAI,CAAC;QACH,OAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,CAA2B,EAAE,OAAO,KAAK,mBAAmB,CAAA;IACvF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC","sourcesContent":["/**\n * Which modeller opens a figure — by the format of its source, which the server reports (`format`).\n *\n * The catalogue sent every figure to the V2 modeller, so a converted figure could only be opened by typing the V3\n * page's address (2026-09-24). A new figure starts in V3: new V2 figures would only add to what the cutover has to\n * convert (ADR-0087). A figure is opened in V3 only when the server says it is V3 — an unknown format goes to the\n * V2 page, which reports what it could not read.\n */\nexport const V3_MODELLER = 'figure-v3-modeller'\nexport const V2_MODELLER = 'figure-modeller'\n\nexport function modellerPathOf(figure?: { id?: string; format?: string | null }): string {\n if (!figure?.id) return V3_MODELLER\n return figure.format === 'v3' ? `${V3_MODELLER}/${figure.id}` : `${V2_MODELLER}/${figure.id}`\n}\n\n/** The version a V3 asset stamps on itself. The V2 page forwards such a figure rather than failing on it. */\nexport function isV3Source(source?: string | null): boolean {\n if (!source) return false\n try {\n return (JSON.parse(source) as { version?: unknown })?.version === 'figure-v3-asset-1'\n } catch {\n return false\n }\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { V3Asset, V3InputChange, V3WorldBox } from '@hatiolab/figure-model';
|
|
2
|
+
import type { GizmoReach } from './v3-gizmo.js';
|
|
3
|
+
/**
|
|
4
|
+
* 맞추기 — the V2 inspector's align tools on a V3 part (parity row ③): centre on X, centre on Z, stand on the floor,
|
|
5
|
+
* mirror on X or Z, snap to the grid.
|
|
6
|
+
*
|
|
7
|
+
* V2 measured against its base box, which ran from 0 to its size on each axis. A V3 asset's frame has the floor at
|
|
8
|
+
* y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on
|
|
9
|
+
* y 0. The part's box is its evaluated, rotated extent (`v3OccupiedBoxesOf`), so a turned part lands by what it
|
|
10
|
+
* actually covers, not by its unturned size.
|
|
11
|
+
*
|
|
12
|
+
* Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only
|
|
13
|
+
* where the gizmo could (`GizmoReach`).
|
|
14
|
+
*/
|
|
15
|
+
export type AlignTool = 'centre-x' | 'centre-z' | 'floor' | 'snap';
|
|
16
|
+
export declare function boxOf(asset: V3Asset, part: string): V3WorldBox | undefined;
|
|
17
|
+
/** The grid V2 snaps to: about five steps across the figure, on 1 · 2 · 5 multiples. */
|
|
18
|
+
export declare function gridStepOf(asset: V3Asset): number;
|
|
19
|
+
export declare function alignChanges(asset: V3Asset, part: string, reach: GizmoReach, tool: AlignTool): V3InputChange[];
|
|
20
|
+
/**
|
|
21
|
+
* Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by
|
|
22
|
+
* `duplicate-part` first; this is the position the copy is then given.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mirrorChanges(asset: V3Asset, part: string, copy: string, axis: 'x' | 'z'): V3InputChange[];
|
|
25
|
+
/** Mirroring a part that already sits on the centre plane would put the copy inside it — V2 refuses it too. */
|
|
26
|
+
export declare function canMirror(asset: V3Asset, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { compileV3Asset, v3OccupiedBoxesOf, v3WritersOf } from '@hatiolab/figure-model';
|
|
2
|
+
import { niceStep } from './part-edits.js';
|
|
3
|
+
const AXES = ['x', 'y', 'z'];
|
|
4
|
+
export function boxOf(asset, part) {
|
|
5
|
+
const geometry = compileV3Asset(asset).evaluate().geometry;
|
|
6
|
+
return v3OccupiedBoxesOf(geometry, v3WritersOf(asset)).find(box => box.part === part);
|
|
7
|
+
}
|
|
8
|
+
/** The grid V2 snaps to: about five steps across the figure, on 1 · 2 · 5 multiples. */
|
|
9
|
+
export function gridStepOf(asset) {
|
|
10
|
+
const geometry = compileV3Asset(asset).evaluate().geometry;
|
|
11
|
+
const boxes = v3OccupiedBoxesOf(geometry, v3WritersOf(asset));
|
|
12
|
+
if (!boxes.length)
|
|
13
|
+
return 1;
|
|
14
|
+
const span = Math.max(...AXES.map(axis => {
|
|
15
|
+
const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2));
|
|
16
|
+
const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2));
|
|
17
|
+
return hi - lo;
|
|
18
|
+
}));
|
|
19
|
+
return niceStep(span);
|
|
20
|
+
}
|
|
21
|
+
export function alignChanges(asset, part, reach, tool) {
|
|
22
|
+
if (!reach.translate)
|
|
23
|
+
return [];
|
|
24
|
+
const box = boxOf(asset, part);
|
|
25
|
+
if (!box)
|
|
26
|
+
return [];
|
|
27
|
+
const value = (id) => asset.designInputs[id] ?? 0;
|
|
28
|
+
const set = (input, next) => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) });
|
|
29
|
+
const at = (axis) => `${part}.position.${axis}`;
|
|
30
|
+
switch (tool) {
|
|
31
|
+
case 'centre-x':
|
|
32
|
+
return [set(at('x'), value(at('x')) - box.centre.x)];
|
|
33
|
+
case 'centre-z':
|
|
34
|
+
return [set(at('z'), value(at('z')) - box.centre.z)];
|
|
35
|
+
case 'floor':
|
|
36
|
+
return [set(at('y'), value(at('y')) - (box.centre.y - box.extent.y / 2))];
|
|
37
|
+
case 'snap': {
|
|
38
|
+
const step = gridStepOf(asset);
|
|
39
|
+
const round = (n) => Math.round(n / step) * step;
|
|
40
|
+
const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))));
|
|
41
|
+
/* A size never snaps to 0 — the part would look gone. */
|
|
42
|
+
if (reach.scale && reach.sizes)
|
|
43
|
+
for (const id of reach.sizes)
|
|
44
|
+
changes.push(set(id, Math.max(step, round(value(id)))));
|
|
45
|
+
return changes;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by
|
|
51
|
+
* `duplicate-part` first; this is the position the copy is then given.
|
|
52
|
+
*/
|
|
53
|
+
export function mirrorChanges(asset, part, copy, axis) {
|
|
54
|
+
const box = boxOf(asset, part);
|
|
55
|
+
if (!box)
|
|
56
|
+
return [];
|
|
57
|
+
const id = `${part}.position.${axis}`;
|
|
58
|
+
return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }];
|
|
59
|
+
}
|
|
60
|
+
/** Mirroring a part that already sits on the centre plane would put the copy inside it — V2 refuses it too. */
|
|
61
|
+
export function canMirror(asset, part, axis, reach) {
|
|
62
|
+
const box = reach.translate ? boxOf(asset, part) : undefined;
|
|
63
|
+
return !!box && Math.abs(box.centre[axis]) >= 0.5;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=v3-align.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-align.js","sourceRoot":"","sources":["../../client/modeller/v3-align.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileV3Asset, v3OccupiedBoxesOf, v3WritersOf } from '@hatiolab/figure-model'\nimport type { V3Asset, V3InputChange, V3WorldBox } from '@hatiolab/figure-model'\n\nimport { niceStep } from './part-edits.js'\nimport type { GizmoReach } from './v3-gizmo.js'\n\n/**\n * 맞추기 — the V2 inspector's align tools on a V3 part (parity row ③): centre on X, centre on Z, stand on the floor,\n * mirror on X or Z, snap to the grid.\n *\n * V2 measured against its base box, which ran from 0 to its size on each axis. A V3 asset's frame has the floor at\n * y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`v3OccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'floor' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\nexport function boxOf(asset: V3Asset, part: string): V3WorldBox | undefined {\n const geometry = compileV3Asset(asset).evaluate().geometry\n return v3OccupiedBoxesOf(geometry, v3WritersOf(asset)).find(box => box.part === part)\n}\n\n/** The grid V2 snaps to: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(asset: V3Asset): number {\n const geometry = compileV3Asset(asset).evaluate().geometry\n const boxes = v3OccupiedBoxesOf(geometry, v3WritersOf(asset))\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(asset: V3Asset, part: string, reach: GizmoReach, tool: AlignTool): V3InputChange[] {\n if (!reach.translate) return []\n const box = boxOf(asset, part)\n if (!box) return []\n const value = (id: string) => asset.designInputs[id] ?? 0\n const set = (input: string, next: number): V3InputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'floor':\n return [set(at('y'), value(at('y')) - (box.centre.y - box.extent.y / 2))]\n case 'snap': {\n const step = gridStepOf(asset)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by\n * `duplicate-part` first; this is the position the copy is then given.\n */\nexport function mirrorChanges(asset: V3Asset, part: string, copy: string, axis: 'x' | 'z'): V3InputChange[] {\n const box = boxOf(asset, part)\n if (!box) return []\n const id = `${part}.position.${axis}`\n return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }]\n}\n\n/** Mirroring a part that already sits on the centre plane would put the copy inside it — V2 refuses it too. */\nexport function canMirror(asset: V3Asset, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean {\n const box = reach.translate ? boxOf(asset, part) : undefined\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import type { GizmoReach } from './v3-gizmo.js';
|
|
2
3
|
import { type V3RenderPart } from './v3-renderer.js';
|
|
3
4
|
/** Renders evaluated V3 geometry directly. There is no parts/base or things-scene conversion. */
|
|
4
5
|
export declare class FigureV3Canvas extends LitElement {
|
|
@@ -6,21 +7,44 @@ export declare class FigureV3Canvas extends LitElement {
|
|
|
6
7
|
parts: V3RenderPart[];
|
|
7
8
|
palette: Record<string, string>;
|
|
8
9
|
context: string;
|
|
10
|
+
/** The selected part, and what the gizmo may write for it (`gizmoReachOf`). Nothing reachable, no gizmo. */
|
|
11
|
+
selected: string;
|
|
12
|
+
reach: GizmoReach;
|
|
13
|
+
/** Move · turn · size — W · E · R, as in the V2 modeller. */
|
|
14
|
+
mode: 'translate' | 'rotate' | 'scale';
|
|
15
|
+
private transform?;
|
|
16
|
+
/** What the gizmo holds: a stand-in at the body's origin, so the body's own matrix stays the evaluation's. */
|
|
17
|
+
private readonly proxy;
|
|
18
|
+
private dragStart?;
|
|
19
|
+
/** The pointerup that ends a drag is not a pick — releasing off the body would otherwise deselect it. */
|
|
20
|
+
private justDragged;
|
|
9
21
|
private renderer?;
|
|
10
22
|
private readonly scene;
|
|
11
23
|
private readonly group;
|
|
12
24
|
private readonly camera;
|
|
13
25
|
private controls?;
|
|
14
26
|
private observer?;
|
|
27
|
+
private themeObserver?;
|
|
15
28
|
private initialized;
|
|
16
29
|
private contextAtRender;
|
|
17
30
|
render(): import("lit-html").TemplateResult<1>;
|
|
31
|
+
private key;
|
|
18
32
|
protected firstUpdated(): void;
|
|
19
33
|
connectedCallback(): void;
|
|
20
34
|
private initialize;
|
|
21
35
|
protected updated(changed: PropertyValues): void;
|
|
36
|
+
/** Puts the gizmo on the selected body, in a mode it may write — or takes it away. */
|
|
37
|
+
private placeGizmo;
|
|
38
|
+
private dragging;
|
|
39
|
+
/** While dragging, the body follows the stand-in; the next evaluation replaces it with where the inputs put it. */
|
|
40
|
+
private follow;
|
|
22
41
|
private updateGeometry;
|
|
23
42
|
fit(): void;
|
|
43
|
+
/**
|
|
44
|
+
* The scene's background, taken from the host's computed background — the token resolved and normalised to
|
|
45
|
+
* `rgb(r, g, b)`, which THREE.Color reads. The token's own text may use the space-separated form it does not.
|
|
46
|
+
*/
|
|
47
|
+
private paintBackground;
|
|
24
48
|
private draw;
|
|
25
49
|
private pick;
|
|
26
50
|
thumbnail(): any;
|