@things-factory/figure-ui 10.1.150 → 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 -15
- 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.js +16 -16
- 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 +6 -6
- 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 +104 -0
- 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'
|
|
@@ -803,10 +803,8 @@ export class FigureEditor extends LitElement {
|
|
|
803
803
|
private add(kind: FigurePaletteKind) {
|
|
804
804
|
if (!this.session) return
|
|
805
805
|
this.run(() => {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
while (ids.has(`${kind}-${n}`)) n++
|
|
809
|
-
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))
|
|
810
808
|
this.session!.add(part)
|
|
811
809
|
this.selected = part.id
|
|
812
810
|
})
|
|
@@ -838,8 +836,7 @@ export class FigureEditor extends LitElement {
|
|
|
838
836
|
if (!choice) return
|
|
839
837
|
const node = upload.summary.nodes.find(n => n.index === (choice as { node?: number }).node)
|
|
840
838
|
this.run(() => {
|
|
841
|
-
const
|
|
842
|
-
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, ''))
|
|
843
840
|
this.session!.add(meshPartOf(upload, choice as { node?: number }, id))
|
|
844
841
|
this.selected = id
|
|
845
842
|
})
|
|
@@ -850,9 +847,8 @@ export class FigureEditor extends LitElement {
|
|
|
850
847
|
*/
|
|
851
848
|
private duplicatePart(id: string) {
|
|
852
849
|
this.run(() => {
|
|
853
|
-
|
|
854
|
-
this.session!.edit({ kind: 'duplicate-part', target: id })
|
|
855
|
-
this.selected = this.session!.read().geometry.find(g => !before.has(g.id))?.id ?? 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
|
|
856
852
|
})
|
|
857
853
|
}
|
|
858
854
|
private movePart(id: string, to: number) {
|
|
@@ -1032,9 +1028,7 @@ export class FigureEditor extends LitElement {
|
|
|
1032
1028
|
private mirror(axis: 'x' | 'z') {
|
|
1033
1029
|
this.run(() => {
|
|
1034
1030
|
const id = this.selected
|
|
1035
|
-
const
|
|
1036
|
-
this.session!.edit({ kind: 'duplicate-part', target: id })
|
|
1037
|
-
const copy = this.session!.read().geometry.find(g => !before.has(g.id))?.id
|
|
1031
|
+
const copy = this.session!.edit({ kind: 'duplicate-part', target: id }).made?.[0]
|
|
1038
1032
|
if (!copy) return
|
|
1039
1033
|
this.session!.apply(mirrorChanges(this.shown()!, id, copy, axis))
|
|
1040
1034
|
this.selected = copy
|
|
@@ -1657,8 +1651,14 @@ export class FigureEditor extends LitElement {
|
|
|
1657
1651
|
return
|
|
1658
1652
|
}
|
|
1659
1653
|
const view = this.session.read()
|
|
1660
|
-
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
|
|
1661
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
|
|
1662
1662
|
this.previewTimer = setTimeout(() => this.computePreview(candidate), PREVIEW_SETTLE_MS)
|
|
1663
1663
|
}
|
|
1664
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
|
|
@@ -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';
|
|
@@ -794,11 +794,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
794
794
|
if (!this.session)
|
|
795
795
|
return;
|
|
796
796
|
this.run(() => {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
while (ids.has(`${kind}-${n}`))
|
|
800
|
-
n++;
|
|
801
|
-
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));
|
|
802
799
|
this.session.add(part);
|
|
803
800
|
this.selected = part.id;
|
|
804
801
|
});
|
|
@@ -832,8 +829,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
832
829
|
return;
|
|
833
830
|
const node = upload.summary.nodes.find(n => n.index === choice.node);
|
|
834
831
|
this.run(() => {
|
|
835
|
-
const
|
|
836
|
-
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, ''));
|
|
837
833
|
this.session.add(meshPartOf(upload, choice, id));
|
|
838
834
|
this.selected = id;
|
|
839
835
|
});
|
|
@@ -844,9 +840,8 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
844
840
|
*/
|
|
845
841
|
duplicatePart(id) {
|
|
846
842
|
this.run(() => {
|
|
847
|
-
|
|
848
|
-
this.session.edit({ kind: 'duplicate-part', target: id });
|
|
849
|
-
this.selected = this.session.read().geometry.find(g => !before.has(g.id))?.id ?? 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;
|
|
850
845
|
});
|
|
851
846
|
}
|
|
852
847
|
movePart(id, to) {
|
|
@@ -1045,9 +1040,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1045
1040
|
mirror(axis) {
|
|
1046
1041
|
this.run(() => {
|
|
1047
1042
|
const id = this.selected;
|
|
1048
|
-
const
|
|
1049
|
-
this.session.edit({ kind: 'duplicate-part', target: id });
|
|
1050
|
-
const copy = this.session.read().geometry.find(g => !before.has(g.id))?.id;
|
|
1043
|
+
const copy = this.session.edit({ kind: 'duplicate-part', target: id }).made?.[0];
|
|
1051
1044
|
if (!copy)
|
|
1052
1045
|
return;
|
|
1053
1046
|
this.session.apply(mirrorChanges(this.shown(), id, copy, axis));
|
|
@@ -1671,9 +1664,16 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1671
1664
|
return;
|
|
1672
1665
|
}
|
|
1673
1666
|
const view = this.session.read();
|
|
1674
|
-
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;
|
|
1675
1669
|
if (same)
|
|
1676
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;
|
|
1677
1677
|
this.previewTimer = setTimeout(() => this.computePreview(candidate), PREVIEW_SETTLE_MS);
|
|
1678
1678
|
}
|
|
1679
1679
|
computePreview(candidate) {
|