@things-factory/figure-ui 10.1.151 → 10.1.152
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 +15 -23
- package/client/modeller/fasten-geometry.ts +32 -0
- package/client/modeller/figure-links.ts +32 -14
- package/client/modeller/mesh-import.ts +0 -10
- package/client/modeller/refusals.ts +14 -1
- package/dist-client/modeller/editor.d.ts +0 -6
- package/dist-client/modeller/editor.js +16 -24
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +13 -0
- package/dist-client/modeller/fasten-geometry.js +29 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -2
- package/dist-client/modeller/figure-links.js +35 -14
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +0 -2
- package/dist-client/modeller/mesh-import.js +0 -11
- package/dist-client/modeller/mesh-import.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +4 -28
- package/dist-client/modeller/refusals.d.ts +2 -1
- package/dist-client/modeller/refusals.js +17 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/browser/build.mjs +7 -2
- package/test/browser/editing.check.mjs +14 -0
- package/test/browser/fix-alignment.check.mjs +101 -0
- package/test/browser/links.check.mjs +34 -5
- package/test/browser/mesh-select.check.mjs +91 -0
- package/test/browser/modeller-page.js +36 -0
- package/test/browser/modeller-revision.check.mjs +18 -6
- package/test/browser/part-ids.check.mjs +24 -6
- package/test/browser/parts.check.mjs +39 -2
- package/test/browser/row-preview.check.mjs +81 -0
- package/test/browser/screen.check.mjs +2 -1
- package/test/fields.test.ts +5 -9
- package/test/fix-alignment.test.ts +32 -0
- package/test/mesh-import.test.ts +1 -8
- package/test/parts-wiring.test.ts +1 -5
- package/test/refusals.test.ts +28 -0
- package/translations/en.json +8 -0
- package/translations/es.json +8 -0
- package/translations/ja.json +8 -0
- package/translations/ko.json +8 -0
- package/translations/ms.json +8 -0
- package/translations/zh.json +8 -0
|
@@ -6,7 +6,7 @@ import { notify, openPopup } from '@operato/layout'
|
|
|
6
6
|
import { OxPrompt } from '@operato/popup'
|
|
7
7
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
8
8
|
import { live } from 'lit/directives/live.js'
|
|
9
|
-
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact, type FigureClipLoss, figurePartFaceOf, type FigureFaceArea, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model'
|
|
9
|
+
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, type FigurePlaneContact, type FigureClipLoss, figurePartFaceOf, type FigureFaceArea, figureRowOf, type FigureRowReading, figureFaceOverhangOf, type FigureFaceOverhang, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model'
|
|
10
10
|
import type { RecipeTemplate } from './recipes.js'
|
|
11
11
|
import './figure-recipe-catalog.js'
|
|
12
12
|
import type { FigureAsset, FigureAuthoringAction, FigureEvaluatedShape, FigureInputChange, FigurePartEdit, FigurePrimitiveCreation, FigureProposalStep, FigureRecipe, FigureRecipeValue, FigurePlacement, FigureFaceMappingNeed, FigureFaceRemap } from '@hatiolab/figure-model'
|
|
@@ -35,7 +35,7 @@ import { ScrollbarStyles } from '@operato/styles'
|
|
|
35
35
|
import { alignChanges, 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 { meshChoicesOf,
|
|
38
|
+
import { meshChoicesOf, meshPartOf } from './mesh-import.js'
|
|
39
39
|
import './figure-mesh-choice.js'
|
|
40
40
|
import { uploadFigureMesh } from '../graphql/index.js'
|
|
41
41
|
import { slotMarksOf } from './slot-layout-host.js'
|
|
@@ -750,14 +750,6 @@ export class FigureEditor extends LitElement {
|
|
|
750
750
|
private shown(): FigureRevision | undefined {
|
|
751
751
|
return this.session?.revisionNow()
|
|
752
752
|
}
|
|
753
|
-
/**
|
|
754
|
-
* The figure's parts by id, one per authored part as the parts list shows them. Not the drawn bodies: a row's copies
|
|
755
|
-
* are drawn under ids of their own, so a part made into a row was missing from those and its id was offered to the
|
|
756
|
-
* next part, which figure-model refused (DUPLICATE_PART).
|
|
757
|
-
*/
|
|
758
|
-
private partIds(): Set<string> {
|
|
759
|
-
return new Set(this.session?.revisionNow()?.asset.appearance.map(look => look.target) ?? [])
|
|
760
|
-
}
|
|
761
753
|
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
762
754
|
private hold(values: Record<string, number>, edit?: FigurePartEdit) {
|
|
763
755
|
this.live = { values: { ...this.live.values, ...values }, ...(edit ? { edit } : this.live.edit ? { edit: this.live.edit } : {}) }
|
|
@@ -811,10 +803,8 @@ export class FigureEditor extends LitElement {
|
|
|
811
803
|
private add(kind: FigurePaletteKind) {
|
|
812
804
|
if (!this.session) return
|
|
813
805
|
this.run(() => {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
while (ids.has(`${kind}-${n}`)) n++
|
|
817
|
-
const part = paletteRecipe(kind, `${kind}-${n}`)
|
|
806
|
+
/* The id is figure-model's to choose: what is in use is its rule, not a list the screen keeps. */
|
|
807
|
+
const part = paletteRecipe(kind, figurePartIdFor(this.session!.revisionNow()!.asset, kind))
|
|
818
808
|
this.session!.add(part)
|
|
819
809
|
this.selected = part.id
|
|
820
810
|
})
|
|
@@ -846,8 +836,7 @@ export class FigureEditor extends LitElement {
|
|
|
846
836
|
if (!choice) return
|
|
847
837
|
const node = upload.summary.nodes.find(n => n.index === (choice as { node?: number }).node)
|
|
848
838
|
this.run(() => {
|
|
849
|
-
const
|
|
850
|
-
const id = meshPartIdOf(node?.name ?? file.name.replace(/\.glb$/i, ''), taken)
|
|
839
|
+
const id = figurePartIdFor(this.session!.revisionNow()!.asset, node?.name ?? file.name.replace(/\.glb$/i, ''))
|
|
851
840
|
this.session!.add(meshPartOf(upload, choice as { node?: number }, id))
|
|
852
841
|
this.selected = id
|
|
853
842
|
})
|
|
@@ -858,9 +847,8 @@ export class FigureEditor extends LitElement {
|
|
|
858
847
|
*/
|
|
859
848
|
private duplicatePart(id: string) {
|
|
860
849
|
this.run(() => {
|
|
861
|
-
|
|
862
|
-
this.session!.edit({ kind: 'duplicate-part', target: id })
|
|
863
|
-
this.selected = [...this.partIds()].find(part => !before.has(part)) ?? id
|
|
850
|
+
/* The copy is the part the edit made, as figure-model named it. */
|
|
851
|
+
this.selected = this.session!.edit({ kind: 'duplicate-part', target: id }).made?.[0] ?? id
|
|
864
852
|
})
|
|
865
853
|
}
|
|
866
854
|
private movePart(id: string, to: number) {
|
|
@@ -1040,9 +1028,7 @@ export class FigureEditor extends LitElement {
|
|
|
1040
1028
|
private mirror(axis: 'x' | 'z') {
|
|
1041
1029
|
this.run(() => {
|
|
1042
1030
|
const id = this.selected
|
|
1043
|
-
const
|
|
1044
|
-
this.session!.edit({ kind: 'duplicate-part', target: id })
|
|
1045
|
-
const copy = [...this.partIds()].find(part => !before.has(part))
|
|
1031
|
+
const copy = this.session!.edit({ kind: 'duplicate-part', target: id }).made?.[0]
|
|
1046
1032
|
if (!copy) return
|
|
1047
1033
|
this.session!.apply(mirrorChanges(this.shown()!, id, copy, axis))
|
|
1048
1034
|
this.selected = copy
|
|
@@ -1665,8 +1651,14 @@ export class FigureEditor extends LitElement {
|
|
|
1665
1651
|
return
|
|
1666
1652
|
}
|
|
1667
1653
|
const view = this.session.read()
|
|
1668
|
-
const
|
|
1654
|
+
const key = JSON.stringify(candidate)
|
|
1655
|
+
const same = this.preview && this.preview.key === key && this.preview.context === view.context && this.preview.part === this.selected && 'revision' in this.preview && this.preview.revision === view.revision
|
|
1669
1656
|
if (same) return
|
|
1657
|
+
/*
|
|
1658
|
+
What another command would do is not said of this one while this one is worked out: a row's pitch refused stayed
|
|
1659
|
+
on screen for the count chosen after it (:3300, 2026-10-11).
|
|
1660
|
+
*/
|
|
1661
|
+
if (this.preview && this.preview.key !== key) this.preview = undefined
|
|
1670
1662
|
this.previewTimer = setTimeout(() => this.computePreview(candidate), PREVIEW_SETTLE_MS)
|
|
1671
1663
|
}
|
|
1672
1664
|
private computePreview(candidate: { action: FigureAuthoringAction; face?: { part: string; face: string } }) {
|
|
@@ -125,6 +125,38 @@ export function alignmentAt(d: number, face: number | undefined, part: number, e
|
|
|
125
125
|
return { how: 'mm', mm: Math.round(d) }
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The alignment that brings the footprint inside the face, for 정렬·간격 고치기: on each axis it comes past an edge, the
|
|
130
|
+
* least move -- to that edge from inside (`min` / `max`, or that distance where edges are not offered) -- or the centre
|
|
131
|
+
* when the part is wider than the face and cannot be inside. `inside` when nothing of it is past an edge: what is out
|
|
132
|
+
* is something fastened on it. `unmeasured` when the face has no size to measure against.
|
|
133
|
+
*/
|
|
134
|
+
export function insideAlignmentOf(
|
|
135
|
+
view: FaceView | undefined,
|
|
136
|
+
align: Partial<Record<Axis, Alignment>>,
|
|
137
|
+
alignMm: Partial<Record<Axis, number>>
|
|
138
|
+
): { align: Partial<Record<Axis, Alignment>>; alignMm: Partial<Record<Axis, number>>; moved: Axis[] } | { cannot: 'inside' | 'unmeasured' } {
|
|
139
|
+
if (!view || view.axes.some(a => view.face[a] === undefined)) return { cannot: 'unmeasured' }
|
|
140
|
+
const next = { align: { ...align }, alignMm: { ...alignMm }, moved: [] as Axis[] }
|
|
141
|
+
for (const a of view.axes) {
|
|
142
|
+
const face = view.face[a]!,
|
|
143
|
+
part = view.part[a]
|
|
144
|
+
const at = offsetOf(align[a], alignMm[a], face, part)
|
|
145
|
+
const low = -face / 2 - (at - part / 2) > 0.5,
|
|
146
|
+
high = at + part / 2 - face / 2 > 0.5
|
|
147
|
+
if (!low && !high) continue
|
|
148
|
+
next.moved.push(a)
|
|
149
|
+
if (part > face) {
|
|
150
|
+
next.align[a] = 'centre'
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
const edge = ((low ? -1 : 1) * (face - part)) / 2
|
|
154
|
+
if (view.edges[a]) next.align[a] = low ? 'min' : 'max'
|
|
155
|
+
else ((next.align[a] = 'mm'), (next.alignMm[a] = Math.round(edge)))
|
|
156
|
+
}
|
|
157
|
+
return next.moved.length ? next : { cannot: 'inside' }
|
|
158
|
+
}
|
|
159
|
+
|
|
128
160
|
/** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */
|
|
129
161
|
export function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {
|
|
130
162
|
const sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> = []
|
|
@@ -6,7 +6,7 @@ import './figure-slot-layout-panel.js'
|
|
|
6
6
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
7
7
|
|
|
8
8
|
import { i18next, localize } from '@operato/i18n'
|
|
9
|
-
import { FIGURE_FACES } from '@hatiolab/figure-model'
|
|
9
|
+
import { FIGURE_FACES, figureFaceOverhangOf } from '@hatiolab/figure-model'
|
|
10
10
|
import type { FigureAsset, FigureAuthoringAction, FigureDimensionSource, FigureFace } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
import {
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
57
57
|
import './figure-fasten-drawing.js'
|
|
58
58
|
import type { FastenChange } from './figure-fasten-drawing.js'
|
|
59
|
-
import { faceViewOf, modeOf, myFaceFor, turnsInPlace, type FastenMode } from './fasten-geometry.js'
|
|
59
|
+
import { faceViewOf, insideAlignmentOf, modeOf, myFaceFor, turnsInPlace, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
60
60
|
import type { FigureRevision } from './revision.js'
|
|
61
61
|
import { HelpToggles } from './help.js'
|
|
62
62
|
import { jointAxisWord } from './joint-axis.js'
|
|
@@ -186,6 +186,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
186
186
|
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
187
187
|
@state() private spotlit = false
|
|
188
188
|
@state() private fasten = freshFasten()
|
|
189
|
+
/** Why 정렬·간격 고치기 moved nothing, said under it; empty once it moved the part or the part changed. */
|
|
190
|
+
@state() private fixNote = ''
|
|
189
191
|
@state() private motion: MotionForm = freshMotion()
|
|
190
192
|
@state() private place = freshPlace()
|
|
191
193
|
/** How places are made here: one at a time, or by the figure's rule. Unset: by the rule when the figure has one. */
|
|
@@ -201,7 +203,9 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
201
203
|
/* The part the forms were loaded for. A panel made with a part already chosen loaded nothing, and said "editing the
|
|
202
204
|
fastening it has" over an empty target (2026-09-27, :3391, the doors). */
|
|
203
205
|
private loadedFor?: string
|
|
204
|
-
willUpdate(
|
|
206
|
+
willUpdate(changed: Map<string, unknown>) {
|
|
207
|
+
/* What 고치기 said is about the figure as it was: another step or another part, and it no longer holds. */
|
|
208
|
+
if (changed.has('asset') || changed.has('partId')) this.fixNote = ''
|
|
205
209
|
if (this.asset && this.partId && this.partId !== this.loadedFor) {
|
|
206
210
|
this.loadedFor = this.partId
|
|
207
211
|
/* A part's own fastening is not another part's choices: the form opens on it (walk 1, 2026-09-26). */
|
|
@@ -430,16 +434,27 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
430
434
|
* holding places instead).
|
|
431
435
|
*/
|
|
432
436
|
/** The face the part sits on, in view with the focus on it: where the place across the face is dragged or keyed. */
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
437
|
+
/**
|
|
438
|
+
* 정렬·간격 고치기: the part moved inside the face it is fastened on, as one step -- the least move on each side it is
|
|
439
|
+
* out, or centred when it is wider than the face. When nothing of it can be moved, why is said under the button, not
|
|
440
|
+
* nothing at all (:3300, 2026-10-11: the button only scrolled to the drawing, which was already in view).
|
|
441
|
+
*/
|
|
442
|
+
private async fixAlignment(view: FaceView | undefined, target: string) {
|
|
443
|
+
/* Whether it is out is the kernel's measure (a round face is not measured there); the drawing's sizes say how far to move. */
|
|
444
|
+
const own = this.revision ? figureFaceOverhangOf(this.revision.asset, this.partId, this.revision.inputs) : undefined
|
|
445
|
+
const fixed =
|
|
446
|
+
own?.state === 'unknown'
|
|
447
|
+
? ({ cannot: 'unmeasured' } as const)
|
|
448
|
+
: own?.state === 'out' || own?.state === 'possible'
|
|
449
|
+
? insideAlignmentOf(view, this.fasten.align, this.fasten.alignMm)
|
|
450
|
+
: ({ cannot: 'inside' } as const)
|
|
451
|
+
if ('cannot' in fixed) {
|
|
452
|
+
this.fixNote = i18next.t(`figure.text.fix-alignment-${fixed.cannot}`, { part: this.partId, on: target })
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
this.fixNote = ''
|
|
456
|
+
this.fasten = { ...this.fasten, align: fixed.align, alignMm: fixed.alignMm }
|
|
457
|
+
await this.onFastenRelease(true)
|
|
443
458
|
}
|
|
444
459
|
|
|
445
460
|
/** The fastening form, open on this part and in view: for "fasten it first" from elsewhere. */
|
|
@@ -600,7 +615,10 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
600
615
|
? html`<div overhang role="status">
|
|
601
616
|
${this.overhangWords.map(line => html`<p>${line}</p>`)}
|
|
602
617
|
${current
|
|
603
|
-
? html`<button act @click=${() => this.
|
|
618
|
+
? html`<button act fix-alignment ?disabled=${this.locked} @click=${() => this.fixAlignment(view, held ? f.target : '')}>
|
|
619
|
+
<md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}
|
|
620
|
+
</button>
|
|
621
|
+
${this.fixNote ? html`<p hint fix-note>${this.fixNote}</p>` : nothing}`
|
|
604
622
|
: nothing}
|
|
605
623
|
</div>`
|
|
606
624
|
: nothing}
|
|
@@ -38,16 +38,6 @@ export function meshChoicesOf(upload: MeshUpload): Array<{ node?: number; name:
|
|
|
38
38
|
]
|
|
39
39
|
}
|
|
40
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
41
|
/**
|
|
52
42
|
* 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
43
|
* and -- for a node -- standing where the node stood in the file, so a crane taken apart node by node stands as it did.
|
|
@@ -101,6 +101,9 @@ export function placedRefusalOf(error: unknown, part: string, say: Say): string
|
|
|
101
101
|
|
|
102
102
|
/** A part name refused: not a name, already used, or another part is named under the part. The rule is the kernel's. */
|
|
103
103
|
export function partNameRefusalOf(error: unknown, say: Say): string | null {
|
|
104
|
+
/* A new part named as one already is: the same sentence as a part renamed so. */
|
|
105
|
+
const takenId = detailsOf(error, 'part-id-taken')
|
|
106
|
+
if (takenId) return say('figure.text.part-name-taken', { name: takenId.part })
|
|
104
107
|
if (codeOf(error) !== 'PART_ID') return null
|
|
105
108
|
const invalid = detailsOf(error, 'name-invalid')
|
|
106
109
|
if (invalid) return say('figure.text.part-name-invalid', { name: invalid.name })
|
|
@@ -236,7 +239,8 @@ export function inputRangeRefusalOf(error: unknown, say: Say): string | null {
|
|
|
236
239
|
|
|
237
240
|
/**
|
|
238
241
|
* A row past its limits: a pitch so fine it would hold more copies than a row takes, a row that would expand to more
|
|
239
|
-
* bodies than a figure draws,
|
|
242
|
+
* bodies than a figure draws, a number of copies outside what a row may have or more than its length holds apart, a
|
|
243
|
+
* row inside a row along the same axis, or a part whose face something holds made into a row.
|
|
240
244
|
*/
|
|
241
245
|
export function rowLimitRefusalOf(error: unknown, say: Say): string | null {
|
|
242
246
|
const copies = detailsOf(error, 'copies-over-limit')
|
|
@@ -245,9 +249,18 @@ export function rowLimitRefusalOf(error: unknown, say: Say): string | null {
|
|
|
245
249
|
if (bodies) return say('figure.text.row-geometry-over-limit', { count: bodies.count.toLocaleString(), limit: bodies.limit.toLocaleString() })
|
|
246
250
|
const count = detailsOf(error, 'row-copies-out-of-range')
|
|
247
251
|
if (count) return say('figure.text.row-copies-out-of-range', { count: count.count.toLocaleString(), min: String(count.min), max: String(count.max) })
|
|
252
|
+
const fit = detailsOf(error, 'row-does-not-fit')
|
|
253
|
+
if (fit) return say('figure.text.row-does-not-fit', { count: fit.count.toLocaleString(), length: mmOf(fit.length), extent: mmOf(fit.extent) })
|
|
254
|
+
const axis = detailsOf(error, 'row-same-axis')
|
|
255
|
+
if (axis) return say('figure.text.row-same-axis', { part: axis.part, outer: axis.outer, axis: axis.axis.toUpperCase() })
|
|
256
|
+
const held = detailsOf(error, 'surface-on-row')
|
|
257
|
+
if (held) return say(`figure.text.row-face-held-${heldByOf(held.at)}`, { part: held.part })
|
|
248
258
|
return null
|
|
249
259
|
}
|
|
250
260
|
|
|
261
|
+
/* What holds a face, from the contract path the kernel names it by: the slot layout, a place, the footprint. */
|
|
262
|
+
const heldByOf = (at: string) => (at.startsWith('slotLayout') ? 'slot-layout' : at.startsWith('bindings') ? 'place' : at.startsWith('occupancy') ? 'footprint' : 'other')
|
|
263
|
+
|
|
251
264
|
/** A row's pitch under which the figure, at the length it is authored, would hold more copies than a row takes. */
|
|
252
265
|
export function repeatPitchRefusalOf(error: unknown, say: Say): string | null {
|
|
253
266
|
if (codeOf(error) !== 'INPUT_RANGE') return null
|
|
@@ -146,12 +146,6 @@ export declare class FigureEditor extends LitElement {
|
|
|
146
146
|
* defaults put every other joint back to its saved angle while one slider moved (user, 2026-09-24).
|
|
147
147
|
*/
|
|
148
148
|
private shown;
|
|
149
|
-
/**
|
|
150
|
-
* The figure's parts by id, one per authored part as the parts list shows them. Not the drawn bodies: a row's copies
|
|
151
|
-
* are drawn under ids of their own, so a part made into a row was missing from those and its id was offered to the
|
|
152
|
-
* next part, which figure-model refused (DUPLICATE_PART).
|
|
153
|
-
*/
|
|
154
|
-
private partIds;
|
|
155
149
|
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
156
150
|
private hold;
|
|
157
151
|
private closeSectionEditor;
|
|
@@ -7,7 +7,7 @@ import { notify, openPopup } from '@operato/layout';
|
|
|
7
7
|
import { OxPrompt } from '@operato/popup';
|
|
8
8
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
9
9
|
import { live } from 'lit/directives/live.js';
|
|
10
|
-
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figurePartFaceOf, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model';
|
|
10
|
+
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figurePartFaceOf, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model';
|
|
11
11
|
import './figure-recipe-catalog.js';
|
|
12
12
|
import { captureFigureThumbnail } from './thumbnail.js';
|
|
13
13
|
import { drawableFigureParts } from './renderer.js';
|
|
@@ -32,7 +32,7 @@ import { SidePanelStyles } from './side-panel-styles.js';
|
|
|
32
32
|
import { ScrollbarStyles } from '@operato/styles';
|
|
33
33
|
import { alignChanges, canMirror, mirrorChanges } from './align.js';
|
|
34
34
|
import { paletteRecipe } from './palette.js';
|
|
35
|
-
import { meshChoicesOf,
|
|
35
|
+
import { meshChoicesOf, meshPartOf } from './mesh-import.js';
|
|
36
36
|
import './figure-mesh-choice.js';
|
|
37
37
|
import { uploadFigureMesh } from '../graphql/index.js';
|
|
38
38
|
import { slotMarksOf } from './slot-layout-host.js';
|
|
@@ -737,14 +737,6 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
737
737
|
shown() {
|
|
738
738
|
return this.session?.revisionNow();
|
|
739
739
|
}
|
|
740
|
-
/**
|
|
741
|
-
* The figure's parts by id, one per authored part as the parts list shows them. Not the drawn bodies: a row's copies
|
|
742
|
-
* are drawn under ids of their own, so a part made into a row was missing from those and its id was offered to the
|
|
743
|
-
* next part, which figure-model refused (DUPLICATE_PART).
|
|
744
|
-
*/
|
|
745
|
-
partIds() {
|
|
746
|
-
return new Set(this.session?.revisionNow()?.asset.appearance.map(look => look.target) ?? []);
|
|
747
|
-
}
|
|
748
740
|
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
749
741
|
hold(values, edit) {
|
|
750
742
|
this.live = { values: { ...this.live.values, ...values }, ...(edit ? { edit } : this.live.edit ? { edit: this.live.edit } : {}) };
|
|
@@ -802,11 +794,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
802
794
|
if (!this.session)
|
|
803
795
|
return;
|
|
804
796
|
this.run(() => {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
while (ids.has(`${kind}-${n}`))
|
|
808
|
-
n++;
|
|
809
|
-
const part = paletteRecipe(kind, `${kind}-${n}`);
|
|
797
|
+
/* The id is figure-model's to choose: what is in use is its rule, not a list the screen keeps. */
|
|
798
|
+
const part = paletteRecipe(kind, figurePartIdFor(this.session.revisionNow().asset, kind));
|
|
810
799
|
this.session.add(part);
|
|
811
800
|
this.selected = part.id;
|
|
812
801
|
});
|
|
@@ -840,8 +829,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
840
829
|
return;
|
|
841
830
|
const node = upload.summary.nodes.find(n => n.index === choice.node);
|
|
842
831
|
this.run(() => {
|
|
843
|
-
const
|
|
844
|
-
const id = meshPartIdOf(node?.name ?? file.name.replace(/\.glb$/i, ''), taken);
|
|
832
|
+
const id = figurePartIdFor(this.session.revisionNow().asset, node?.name ?? file.name.replace(/\.glb$/i, ''));
|
|
845
833
|
this.session.add(meshPartOf(upload, choice, id));
|
|
846
834
|
this.selected = id;
|
|
847
835
|
});
|
|
@@ -852,9 +840,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
852
840
|
*/
|
|
853
841
|
duplicatePart(id) {
|
|
854
842
|
this.run(() => {
|
|
855
|
-
|
|
856
|
-
this.session.edit({ kind: 'duplicate-part', target: id });
|
|
857
|
-
this.selected = [...this.partIds()].find(part => !before.has(part)) ?? id;
|
|
843
|
+
/* The copy is the part the edit made, as figure-model named it. */
|
|
844
|
+
this.selected = this.session.edit({ kind: 'duplicate-part', target: id }).made?.[0] ?? id;
|
|
858
845
|
});
|
|
859
846
|
}
|
|
860
847
|
movePart(id, to) {
|
|
@@ -1053,9 +1040,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1053
1040
|
mirror(axis) {
|
|
1054
1041
|
this.run(() => {
|
|
1055
1042
|
const id = this.selected;
|
|
1056
|
-
const
|
|
1057
|
-
this.session.edit({ kind: 'duplicate-part', target: id });
|
|
1058
|
-
const copy = [...this.partIds()].find(part => !before.has(part));
|
|
1043
|
+
const copy = this.session.edit({ kind: 'duplicate-part', target: id }).made?.[0];
|
|
1059
1044
|
if (!copy)
|
|
1060
1045
|
return;
|
|
1061
1046
|
this.session.apply(mirrorChanges(this.shown(), id, copy, axis));
|
|
@@ -1679,9 +1664,16 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1679
1664
|
return;
|
|
1680
1665
|
}
|
|
1681
1666
|
const view = this.session.read();
|
|
1682
|
-
const
|
|
1667
|
+
const key = JSON.stringify(candidate);
|
|
1668
|
+
const same = this.preview && this.preview.key === key && this.preview.context === view.context && this.preview.part === this.selected && 'revision' in this.preview && this.preview.revision === view.revision;
|
|
1683
1669
|
if (same)
|
|
1684
1670
|
return;
|
|
1671
|
+
/*
|
|
1672
|
+
What another command would do is not said of this one while this one is worked out: a row's pitch refused stayed
|
|
1673
|
+
on screen for the count chosen after it (:3300, 2026-10-11).
|
|
1674
|
+
*/
|
|
1675
|
+
if (this.preview && this.preview.key !== key)
|
|
1676
|
+
this.preview = undefined;
|
|
1685
1677
|
this.previewTimer = setTimeout(() => this.computePreview(candidate), PREVIEW_SETTLE_MS);
|
|
1686
1678
|
}
|
|
1687
1679
|
computePreview(candidate) {
|