@things-factory/figure-ui 10.1.271 → 10.1.272
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/canvas.ts +4 -2
- package/client/modeller/clips.ts +74 -4
- package/client/modeller/fasten-form.ts +69 -4
- package/client/modeller/figure-cavity-drawing.ts +1 -3
- package/client/modeller/figure-clips.ts +101 -8
- package/client/modeller/figure-fasten-dialog.ts +41 -30
- package/client/modeller/figure-fasten-drawing.ts +3 -1
- package/client/modeller/figure-first-save.ts +4 -3
- package/client/modeller/figure-links.ts +85 -28
- package/client/modeller/figure-preview.ts +41 -3
- package/client/modeller/figure-size-editor.ts +7 -2
- package/client/modeller/first-save.ts +8 -0
- package/client/modeller/focus-on-render.ts +4 -0
- package/client/modeller/pivot.ts +76 -0
- package/client/modeller/size-response.ts +14 -0
- package/client/modeller/stage-model.ts +50 -6
- package/dist-client/modeller/canvas.d.ts +1 -0
- package/dist-client/modeller/canvas.js +3 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +21 -4
- package/dist-client/modeller/clips.js +69 -4
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +8 -0
- package/dist-client/modeller/fasten-form.js +64 -5
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-cavity-drawing.js +1 -3
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +104 -8
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +39 -24
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +3 -1
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.js +4 -3
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -0
- package/dist-client/modeller/figure-links.js +89 -35
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +39 -3
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.js +7 -3
- package/dist-client/modeller/figure-size-editor.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +5 -0
- package/dist-client/modeller/first-save.js +7 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/focus-on-render.d.ts +2 -0
- package/dist-client/modeller/focus-on-render.js +4 -0
- package/dist-client/modeller/focus-on-render.js.map +1 -0
- package/dist-client/modeller/pivot.d.ts +12 -0
- package/dist-client/modeller/pivot.js +59 -0
- package/dist-client/modeller/pivot.js.map +1 -0
- package/dist-client/modeller/size-response.d.ts +6 -0
- package/dist-client/modeller/size-response.js +14 -0
- package/dist-client/modeller/size-response.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +17 -3
- package/dist-client/modeller/stage-model.js +44 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/browser/editing.check.mjs +6 -5
- package/test/browser/section.check.mjs +21 -0
- package/test/clips.test.ts +49 -4
- package/test/fasten-form.test.ts +44 -0
- package/test/first-save.test.ts +11 -1
- package/test/pivot.test.ts +57 -0
- package/test/size-response.test.ts +10 -2
- package/test/stage-model.test.ts +23 -1
- 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
- package/tutorial/humanoid/README.md +22 -0
- package/tutorial/humanoid/chapters.mjs +247 -0
- package/tutorial/humanoid/encode.mjs +26 -0
- package/tutorial/humanoid/lib.mjs +197 -0
- package/tutorial/humanoid/record.mjs +65 -0
|
@@ -5,7 +5,7 @@ import { create, Model, niceGridStep } from '@hatiolab/things-scene'
|
|
|
5
5
|
import type { GizmoReach, GizmoResult } from './gizmo.js'
|
|
6
6
|
import type { FigureRenderPart } from './renderer.js'
|
|
7
7
|
import { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
8
|
-
import { boardFor, changedKeys, componentOf, gizmoResultOf, needsRefit, type Board } from './stage-model.js'
|
|
8
|
+
import { boardFor, changedKeys, componentOf, drawnSpanOf, gizmoResultOf, needsRefit, outgrowsFit, type Board, type DrawnSpan } from './stage-model.js'
|
|
9
9
|
import { JOINT_FAN } from './joint-fan.js'
|
|
10
10
|
import { SLOT_MARK, SLOT_MARK_CHOSEN } from './slot-layout-host.js'
|
|
11
11
|
import { PREVIEW, TARGET_FACE } from './command-preview.js'
|
|
@@ -112,6 +112,7 @@ export class FigureCanvas extends LitElement {
|
|
|
112
112
|
private board?: Board & { depth: number }
|
|
113
113
|
/** The figure's reach when the view was last fitted to it. */
|
|
114
114
|
private fittedFor?: Board & { depth: number }
|
|
115
|
+
private fittedSpan?: DrawnSpan
|
|
115
116
|
private refitTimer?: ReturnType<typeof setTimeout>
|
|
116
117
|
private standingFor?: string
|
|
117
118
|
private watcher?: ResizeObserver
|
|
@@ -174,6 +175,7 @@ export class FigureCanvas extends LitElement {
|
|
|
174
175
|
/** The whole figure in view: the board's fit, then the 3D camera's own. */
|
|
175
176
|
fit() {
|
|
176
177
|
this.fittedFor = boardFor(this.parts)
|
|
178
|
+
this.fittedSpan = drawnSpanOf(this.parts)
|
|
177
179
|
this.scene?.fit?.('ratio')
|
|
178
180
|
this.scene?.root?.fitCameraToModel?.()
|
|
179
181
|
}
|
|
@@ -203,7 +205,7 @@ export class FigureCanvas extends LitElement {
|
|
|
203
205
|
drawn as it is typed -- 「3」, 「30」, 「300」 on the way to 3000 -- so the reach is judged after the last change.
|
|
204
206
|
*/
|
|
205
207
|
clearTimeout(this.refitTimer)
|
|
206
|
-
if (this.parts.length) this.refitTimer = setTimeout(() => needsRefit(this.fittedFor, boardFor(this.parts)) && this.fit(), REFIT_SETTLE_MS)
|
|
208
|
+
if (this.parts.length) this.refitTimer = setTimeout(() => (needsRefit(this.fittedFor, boardFor(this.parts)) || outgrowsFit(this.fittedSpan, drawnSpanOf(this.parts))) && this.fit(), REFIT_SETTLE_MS)
|
|
207
209
|
return
|
|
208
210
|
}
|
|
209
211
|
this.teardown()
|
package/client/modeller/clips.ts
CHANGED
|
@@ -47,6 +47,12 @@ export function clipsOf(asset: FigureAsset): Clip[] {
|
|
|
47
47
|
})
|
|
48
48
|
clips.set(d.clip, clip)
|
|
49
49
|
}
|
|
50
|
+
/*
|
|
51
|
+
Tracks in the order the controls are listed, not the order their drivers were last written: an edit writes a
|
|
52
|
+
driver anew at the end, and a track keyed jumped to the bottom of its clip (tutorial run 2026-11-12).
|
|
53
|
+
*/
|
|
54
|
+
const order = new Map(asset.document.model.inputs.map((i, n) => [i.id, n]))
|
|
55
|
+
for (const clip of clips.values()) clip.tracks.sort((a, b) => (order.get(a.state) ?? 0) - (order.get(b.state) ?? 0))
|
|
50
56
|
return [...clips.values()]
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -95,6 +101,25 @@ export const groupEdit = (group: ClipGroup, patch: Partial<Omit<ClipGroup, 'id'>
|
|
|
95
101
|
group: { id: group.id, label: patch.label ?? group.label, clips: patch.clips ?? group.clips, defaultClip: patch.defaultClip ?? group.defaultClip, transitionSeconds: patch.transitionSeconds ?? group.transitionSeconds }
|
|
96
102
|
})
|
|
97
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The controls a playing clip drives in the preview, each with that clip: a clip in no group played at a speed above 0,
|
|
106
|
+
* or the clip a motion group plays at a speed above 0 (its default until one is chosen).
|
|
107
|
+
*/
|
|
108
|
+
export function playedControlsOf(
|
|
109
|
+
asset: FigureAsset,
|
|
110
|
+
driving: Record<string, number>,
|
|
111
|
+
moving: Record<string, { clip: string; speed: number }>
|
|
112
|
+
): Map<string, string> {
|
|
113
|
+
const playing = new Set(Object.entries(driving).filter(([, speed]) => speed > 0).map(([clip]) => clip))
|
|
114
|
+
for (const group of asset.motionGroups ?? []) {
|
|
115
|
+
const m = moving[group.id]
|
|
116
|
+
if (m && m.speed > 0) playing.add(m.clip || group.defaultClip)
|
|
117
|
+
}
|
|
118
|
+
const played = new Map<string, string>()
|
|
119
|
+
for (const d of asset.drivers ?? []) if (playing.has(d.clip) && !played.has(d.state)) played.set(d.state, d.clip)
|
|
120
|
+
return played
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */
|
|
99
124
|
export function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[] {
|
|
100
125
|
const units = new Map(asset.document.model.inputs.map(i => [i.id, i.unit]))
|
|
@@ -133,6 +158,25 @@ export function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] |
|
|
|
133
158
|
}
|
|
134
159
|
|
|
135
160
|
/** A name no clip has, for a copy: `walk-copy`, then `walk-copy-2`, … */
|
|
161
|
+
/**
|
|
162
|
+
* A clip copied as one step (review 2026-11-11: a walk copied into a run had to be put in a motion group first, by a
|
|
163
|
+
* section further down, the button grey until then). A copy plays the same controls, so the two are one group: a clip in
|
|
164
|
+
* none is put in a new one, named after it, before it is copied.
|
|
165
|
+
*/
|
|
166
|
+
export function copyClipEdits(asset: FigureAsset, clip: string, named = 'motion'): unknown[] {
|
|
167
|
+
const copy = { kind: 'copy-clip' as const, clip, name: freeClipNameOf(asset, clip) }
|
|
168
|
+
if ((asset.motionGroups ?? []).some(g => g.clips.includes(clip))) return [copy]
|
|
169
|
+
/*
|
|
170
|
+
The group is named for what it is, not after the first clip: the preview reads "<group> clip · walk | run", and a
|
|
171
|
+
group named "walk" read as if run were a kind of walk (tutorial run 2026-11-12).
|
|
172
|
+
*/
|
|
173
|
+
const labels = new Set((asset.motionGroups ?? []).map(g => g.label))
|
|
174
|
+
let label = named
|
|
175
|
+
for (let n = 2; labels.has(label); n++) label = `${named} ${n}`
|
|
176
|
+
const group = { id: freeGroupIdOf(asset), label, clips: [clip], defaultClip: clip, transitionSeconds: 0.2 }
|
|
177
|
+
return [{ kind: 'set-motion-group' as const, group }, copy]
|
|
178
|
+
}
|
|
179
|
+
|
|
136
180
|
export function freeClipNameOf(asset: FigureAsset, clip: string): string {
|
|
137
181
|
const taken = new Set((asset.drivers ?? []).map(d => d.clip))
|
|
138
182
|
const base = `${clip}-copy`
|
|
@@ -215,8 +259,15 @@ export type TrackJoin = { kind: 'free' } | { kind: 'group'; group: string; label
|
|
|
215
259
|
* door that has 「닫기」, and the two become one group, one playing at a time, without having to design the group first.
|
|
216
260
|
* All of it one step of the history.
|
|
217
261
|
*/
|
|
218
|
-
export function newClipEdits(
|
|
219
|
-
|
|
262
|
+
export function newClipEdits(
|
|
263
|
+
asset: FigureAsset,
|
|
264
|
+
state: string,
|
|
265
|
+
clip: string,
|
|
266
|
+
duration: number,
|
|
267
|
+
keys: ClipKey[],
|
|
268
|
+
interpolation?: ClipTrack['interpolation']
|
|
269
|
+
): { edits: unknown[]; join: TrackJoin } {
|
|
270
|
+
const track = { kind: 'set-driver' as const, state, clip, duration, keys, ...(interpolation ? { interpolation } : {}) }
|
|
220
271
|
const writers = [...new Set((asset.drivers ?? []).filter(d => d.state === state).map(d => d.clip))]
|
|
221
272
|
if (!writers.length) return { edits: [track], join: { kind: 'free' } }
|
|
222
273
|
const group = (asset.motionGroups ?? []).find(g => writers.some(c => g.clips.includes(c)))
|
|
@@ -245,7 +296,25 @@ export function rememberedPoseEdits(asset: FigureAsset, clip: string, seconds: n
|
|
|
245
296
|
const edits: unknown[] = []
|
|
246
297
|
for (const track of found.tracks) {
|
|
247
298
|
const now = values[track.state]
|
|
248
|
-
if (now === undefined
|
|
299
|
+
if (now === undefined) continue
|
|
300
|
+
/*
|
|
301
|
+
* A wrapping track's end reads as its start (review 2026-11-11: a walk kept at its end changed nothing, and a pose
|
|
302
|
+
* kept at 0 left the end key behind -- the loop jumped at its seam). So at either end the key there is compared and
|
|
303
|
+
* written itself, and a loop that closed -- start and end alike -- stays closed. A loop left open on purpose (a ramp
|
|
304
|
+
* that starts over) keeps its other end. A track that adds up turns on past its end and is read as before.
|
|
305
|
+
*/
|
|
306
|
+
if (shiftable(track) && (at === 0 || at === found.duration)) {
|
|
307
|
+
const keyAt = (s: number) => track.keys.find(k => k.seconds === s)?.value
|
|
308
|
+
const start = keyAt(0)
|
|
309
|
+
const end = keyAt(found.duration)
|
|
310
|
+
const mine = at === 0 ? start : end
|
|
311
|
+
if (same(now, mine ?? (there.get(track.state) ?? now))) continue
|
|
312
|
+
const closed = start !== undefined && end !== undefined && same(start, end)
|
|
313
|
+
const keys = withKey(track.keys, { seconds: at, value: now })
|
|
314
|
+
edits.push(trackEdit(found, track, { keys: closed ? withKey(keys, { seconds: at === 0 ? found.duration : 0, value: now }) : keys }))
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
if (same(now, there.get(track.state) ?? now)) continue
|
|
249
318
|
edits.push(trackEdit(found, track, { keys: withKey(track.keys, { seconds: at, value: now }) }))
|
|
250
319
|
}
|
|
251
320
|
const free = new Set(undrivenOf(asset, clip))
|
|
@@ -261,7 +330,8 @@ export function rememberedPoseEdits(asset: FigureAsset, clip: string, seconds: n
|
|
|
261
330
|
continue
|
|
262
331
|
}
|
|
263
332
|
const keys = at === 0 ? [{ seconds: 0, value: now }, { seconds: found.duration, value: now }] : [{ seconds: 0, value: was }, { seconds: at, value: now }]
|
|
264
|
-
|
|
333
|
+
/* A track a kept pose starts goes from pose to pose: eased in and out of each, as a shown move is (motion contract). */
|
|
334
|
+
edits.push({ kind: 'set-driver' as const, state: input.id, clip, keys, interpolation: 'smooth' as const })
|
|
265
335
|
}
|
|
266
336
|
return { edits, skipped }
|
|
267
337
|
}
|
|
@@ -5,6 +5,7 @@ import { faceViewOf, facesAsDrawnOf, type FaceView, type FastenMode, type Fasten
|
|
|
5
5
|
import type { FastenChange } from './figure-fasten-drawing.js'
|
|
6
6
|
import {
|
|
7
7
|
alignOf,
|
|
8
|
+
alignAxesOf,
|
|
8
9
|
alignRowsOf,
|
|
9
10
|
canFasten,
|
|
10
11
|
facesOf,
|
|
@@ -70,13 +71,60 @@ export function fastenFormOf(revision: FigureRevision, part: string): FastenForm
|
|
|
70
71
|
const own = fasteningOf(revision, part)
|
|
71
72
|
if (!own || own.atOrigin) {
|
|
72
73
|
/* A part still where it was added was put nowhere by the author: fastened, it goes on the face, no gap to keep. */
|
|
73
|
-
|
|
74
|
-
return !own && standsWhereAdded(pose) ? { ...freshFasten(), gapFrom: 'pressed' } : freshFasten()
|
|
74
|
+
return addedUnplaced(revision, part) ? { ...freshFasten(), gapFrom: 'pressed' } : freshFasten()
|
|
75
75
|
}
|
|
76
76
|
/* A gap the record works out is the author's; a number is where the part stands, so aiming again keeps it there. */
|
|
77
77
|
return { ...freshFasten(), ...own, gapFrom: own.gapKept !== undefined ? 'typed' : 'kept' } as FastenForm
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** A part fastened to nothing that still stands where the palette put it: the author has put it nowhere to keep. */
|
|
81
|
+
function addedUnplaced(revision: FigureRevision, part: string): boolean {
|
|
82
|
+
if (fasteningOf(revision, part)) return false
|
|
83
|
+
const pose = (revision.geometry.find(g => (g as { id: string }).id === part) as { pose?: { r: number[][]; t: number[] } } | undefined)?.pose
|
|
84
|
+
return standsWhereAdded(pose)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The face a part just added starts on when aimed at `target`: the one asked, unless another part is fastened there
|
|
89
|
+
* already -- then the first face of the target nothing is fastened to (review 2026-11-11: a thigh aimed at a pelvis
|
|
90
|
+
* started on its top, where the body stands, and went inside the body until the bottom was chosen). A part the author
|
|
91
|
+
* has placed keeps the face it stands by: a face holds many parts (rollers and a lid on one frame).
|
|
92
|
+
*/
|
|
93
|
+
function freeFaceOf(revision: FigureRevision, part: string, target: string, asked: FigureFace): FigureFace {
|
|
94
|
+
const on = (end: unknown) => end as { part?: string; face?: string }
|
|
95
|
+
const joints = revision.asset.joints ?? []
|
|
96
|
+
const taken = new Set(joints.filter(j => on(j.body0).part === target && on(j.body1).part !== part).map(j => on(j.body0).face))
|
|
97
|
+
/* The face the target itself stands on its own parent by: a thigh hung under a pelvis touches it with its top. */
|
|
98
|
+
let hungBy: FigureFace | undefined
|
|
99
|
+
for (const j of joints)
|
|
100
|
+
if (on(j.body1).part === target && on(j.body0).face && (j as { facing?: string }).facing !== 'flush') {
|
|
101
|
+
hungBy = OPPOSITE[on(j.body0).face as FigureFace]
|
|
102
|
+
taken.add(hungBy)
|
|
103
|
+
}
|
|
104
|
+
/*
|
|
105
|
+
A tall part hung by its side -- an upper arm on a torso -- goes on downward: what is fastened to it starts under its
|
|
106
|
+
bottom end, not on its top beside the shoulder (tutorial run 2026-11-12: a forearm started on the upper arm's top
|
|
107
|
+
and its bottom had to be picked by hand).
|
|
108
|
+
*/
|
|
109
|
+
if (hungBy && FIGURE_FACES[hungBy] !== 'y' && !taken.has('bottom') && tallOf(revision, target)) return 'bottom'
|
|
110
|
+
if (!taken.has(asked)) return asked
|
|
111
|
+
/* The face across from the one taken first: a body on the top, legs under the bottom. */
|
|
112
|
+
const across = OPPOSITE[asked]
|
|
113
|
+
const faces = facesOf(revision.asset, target)
|
|
114
|
+
if (faces.includes(across) && !taken.has(across)) return across
|
|
115
|
+
return (faces.find(face => !taken.has(face)) as FigureFace | undefined) ?? asked
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether a part as drawn is at least half again as tall as it is wide and deep, read off its own faces. */
|
|
119
|
+
function tallOf(revision: FigureRevision, part: string): boolean {
|
|
120
|
+
const faces = (revision.geometry.find(g => (g as { id: string }).id === part) as { shape?: { faces?: Record<string, { at: number }> } } | undefined)?.shape?.faces
|
|
121
|
+
const span = (lo: string, hi: string) => (faces?.[hi] && faces?.[lo] ? Math.abs(faces[hi]!.at - faces[lo]!.at) : undefined)
|
|
122
|
+
const [x, y, z] = [span('left', 'right'), span('bottom', 'top'), span('back', 'front')]
|
|
123
|
+
return x !== undefined && y !== undefined && z !== undefined && y >= 1.5 * Math.max(x, z)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const OPPOSITE: Record<FigureFace, FigureFace> = { top: 'bottom', bottom: 'top', left: 'right', right: 'left', front: 'back', back: 'front' }
|
|
127
|
+
|
|
80
128
|
/** What a part may be fastened to: every other part that can be, each said whether it would close a loop. */
|
|
81
129
|
export function fastenTargetsOf(revision: FigureRevision, part: string): Array<{ part: string; loops: boolean }> {
|
|
82
130
|
const asset = revision.asset
|
|
@@ -127,7 +175,12 @@ export function aimFasten(
|
|
|
127
175
|
'plane' in to
|
|
128
176
|
? { ...form, toPlane: true, target: '', facing: form.toPlane ? form.facing : (figurePlaneFacingOf(revision.asset) ?? form.facing) }
|
|
129
177
|
: 'part' in to
|
|
130
|
-
? {
|
|
178
|
+
? {
|
|
179
|
+
...form,
|
|
180
|
+
toPlane: false,
|
|
181
|
+
target: to.part,
|
|
182
|
+
targetFace: to.part !== form.target && addedUnplaced(revision, part) ? freeFaceOf(revision, part, to.part, form.targetFace) : form.targetFace
|
|
183
|
+
}
|
|
131
184
|
: { ...form, targetFace: to.targetFace }
|
|
132
185
|
const theirFaces = f.target ? facesOf(revision.asset, f.target) : FACES
|
|
133
186
|
const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]) as FigureFace
|
|
@@ -136,7 +189,13 @@ export function aimFasten(
|
|
|
136
189
|
: f.target
|
|
137
190
|
? { part: f.target, face: targetFace }
|
|
138
191
|
: undefined
|
|
139
|
-
|
|
192
|
+
/*
|
|
193
|
+
* A part still where the palette put it has no place of the author's to keep: it goes to the face's centre (review
|
|
194
|
+
* 2026-11-11: a shin added at the origin and aimed at a thigh 85 mm off it was put 85 mm off the thigh's centre; an
|
|
195
|
+
* arm aimed at the body's side stood off the face, out of reach of the drawing).
|
|
196
|
+
*/
|
|
197
|
+
const centred = { align: {}, alignMm: {}, alignInput: {} }
|
|
198
|
+
return { ...f, ...(aim && !addedUnplaced(revision, part) ? placeKeptOf(revision, part, aim) : centred) }
|
|
140
199
|
}
|
|
141
200
|
|
|
142
201
|
/** A change the drawing asks: a face, a way, a gap or a place across the face. */
|
|
@@ -192,6 +251,11 @@ export interface FastenPlan {
|
|
|
192
251
|
view?: FaceView
|
|
193
252
|
/** The axes across the face asked in rows: where the drawing cannot set the place. */
|
|
194
253
|
alignRows: { axis: Axis; edges: boolean }[]
|
|
254
|
+
/**
|
|
255
|
+
* Every axis across the face, the drawing's too: the details ask each by a number as well (review 2026-11-11: a thigh
|
|
256
|
+
* dragged to a pelvis's edge overshot by 47 mm, and dragged back by eye).
|
|
257
|
+
*/
|
|
258
|
+
alignAxes?: { axis: Axis; edges: boolean }[]
|
|
195
259
|
action?: FigureAuthoringAction
|
|
196
260
|
/** The face the command would meet, lit on the canvas while it is tried. */
|
|
197
261
|
face?: { part: string; face: string }
|
|
@@ -251,6 +315,7 @@ function fastenPlanWithout(revision: FigureRevision, part: string, f: FastenForm
|
|
|
251
315
|
mode,
|
|
252
316
|
view,
|
|
253
317
|
alignRows: alignRowsOf(asset, aim, view, f.alignInput),
|
|
318
|
+
alignAxes: alignAxesOf(asset, aim),
|
|
254
319
|
action,
|
|
255
320
|
...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } })
|
|
256
321
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { css, html, LitElement, nothing, svg } from 'lit'
|
|
2
2
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
|
-
import { ref } from 'lit/directives/ref.js'
|
|
4
3
|
|
|
5
4
|
import { i18next, localize } from '@operato/i18n'
|
|
6
5
|
import { figureCavitySectionOf } from '@hatiolab/figure-model'
|
|
7
6
|
import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape, FigureSection } from '@hatiolab/figure-model'
|
|
7
|
+
import { focusOnRender } from './focus-on-render.js'
|
|
8
8
|
import { cavityRefusalWordsOf, cavityWorkingOf, END_FACE, endToggled, mm, SIDE_FACE, wallDragged } from './section-controls.js'
|
|
9
9
|
|
|
10
10
|
type Axis = 'x' | 'y' | 'z'
|
|
11
11
|
|
|
12
|
-
/** A field opened by a press takes the keys at once. */
|
|
13
|
-
const focusOnRender = () => ref(el => el && requestAnimationFrame(() => (el as HTMLInputElement).select()))
|
|
14
12
|
type End = 'start' | 'end'
|
|
15
13
|
type Body = { id: string; shape: FigureEvaluatedShape }
|
|
16
14
|
|
|
@@ -8,7 +8,7 @@ import { i18next, localize } from '@operato/i18n'
|
|
|
8
8
|
import { unitWord } from './unit-words.js'
|
|
9
9
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
|
|
11
|
-
import { clipGroupsOf, withKeyChanged, clipsOf, copiedTrackEdit, copyTargetsOf, freeClipNameOf, freeGroupIdOf, groupEdit, newClipEdits, newTrackKeys, poseAt, rememberedPoseEdits, shiftable, shiftOffKeyOf, trackEdit, undrivenOf, withKey, withoutKey, type Clip, type ClipGroup, type ClipKey, type ClipTrack } from './clips.js'
|
|
11
|
+
import { clipGroupsOf, withKeyChanged, clipsOf, copiedTrackEdit, copyClipEdits, copyTargetsOf, freeClipNameOf, freeGroupIdOf, groupEdit, newClipEdits, newTrackKeys, poseAt, rememberedPoseEdits, shiftable, shiftOffKeyOf, trackEdit, undrivenOf, withKey, withoutKey, type Clip, type ClipGroup, type ClipKey, type ClipTrack } from './clips.js'
|
|
12
12
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
13
13
|
import { HelpToggles } from './help.js'
|
|
14
14
|
|
|
@@ -37,6 +37,51 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
37
37
|
:host {
|
|
38
38
|
display: block;
|
|
39
39
|
}
|
|
40
|
+
/*
|
|
41
|
+
The pose bar: the clip being keyed, its moment and 「현재 자세 기록」, first under the controls and held at the top
|
|
42
|
+
while the clips scroll. With walk and run, run's own button stood under all of walk's tracks, a long scroll from
|
|
43
|
+
the controls typed for it (tutorial run 2026-11-12).
|
|
44
|
+
*/
|
|
45
|
+
div[pose-bar] {
|
|
46
|
+
position: sticky;
|
|
47
|
+
top: 0;
|
|
48
|
+
z-index: 1;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-wrap: wrap;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 6px 10px;
|
|
53
|
+
margin-bottom: 8px;
|
|
54
|
+
padding: 8px;
|
|
55
|
+
background: var(--md-sys-color-surface-container, var(--md-sys-color-surface));
|
|
56
|
+
border: 1px solid var(--md-sys-color-primary);
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
}
|
|
59
|
+
div[pose-bar] span[clip-choices] {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-wrap: wrap;
|
|
62
|
+
gap: 4px;
|
|
63
|
+
}
|
|
64
|
+
div[pose-bar] span[clip-choices] button {
|
|
65
|
+
font: inherit;
|
|
66
|
+
color: inherit;
|
|
67
|
+
padding: 1px 10px;
|
|
68
|
+
background: transparent;
|
|
69
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
70
|
+
border-radius: 10px;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
div[pose-bar] span[clip-choices] button[aria-pressed='true'] {
|
|
74
|
+
background: var(--md-sys-color-secondary-container);
|
|
75
|
+
border-color: var(--md-sys-color-secondary);
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
}
|
|
78
|
+
div[pose-bar] label[at] input {
|
|
79
|
+
width: 64px;
|
|
80
|
+
}
|
|
81
|
+
div[pose-bar] p[hint] {
|
|
82
|
+
flex-basis: 100%;
|
|
83
|
+
margin: 0;
|
|
84
|
+
}
|
|
40
85
|
section[clip] {
|
|
41
86
|
display: grid;
|
|
42
87
|
gap: 8px;
|
|
@@ -158,6 +203,8 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
158
203
|
|
|
159
204
|
/** Where each clip is scrubbed to, in seconds. */
|
|
160
205
|
@state() private at: Record<string, number> = {}
|
|
206
|
+
/** The clip the pose bar keys: the one last scrubbed. */
|
|
207
|
+
@state() private active = ''
|
|
161
208
|
@state() private adding = { state: '', clip: '', name: '', duration: 2 }
|
|
162
209
|
@state() private grouping = { clip: '', label: '' }
|
|
163
210
|
/** The track whose copy is being set up: onto which control, how many seconds later. */
|
|
@@ -180,6 +227,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
180
227
|
/* Nothing to play yet: said inside the section it belongs to, with the section's own margins. */
|
|
181
228
|
const empty = !clips.length && !undrivenOf(asset).length && !asset.motionGroups?.length
|
|
182
229
|
return html`
|
|
230
|
+
${clips.length ? this.renderPoseBar(clips) : nothing}
|
|
183
231
|
<section>
|
|
184
232
|
<h3>${i18next.t('figure.label.clips')}${this.help.button('figure.text.clips-explained')}</h3>
|
|
185
233
|
${this.help.hint('figure.text.clips-explained')}
|
|
@@ -198,12 +246,16 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
198
246
|
return html`<section clip>
|
|
199
247
|
<div form>
|
|
200
248
|
<span>${i18next.t('figure.label.clip-name')}</span>
|
|
201
|
-
<input .value=${clip.name} ?disabled=${this.locked} @change=${(e: Event) => this.
|
|
249
|
+
<input .value=${clip.name} ?disabled=${this.locked} @change=${(e: Event) => this.rename(clip, (e.target as HTMLInputElement).value)} />
|
|
202
250
|
<span>${i18next.t('figure.label.clip-duration')}</span>
|
|
203
251
|
<input type="number" min="0.1" step="any" .value=${String(clip.duration)} ?disabled=${this.locked} @change=${(e: Event) => this.edit({ kind: 'set-clip', clip: clip.name, duration: number(e) })} />
|
|
204
252
|
</div>
|
|
205
|
-
<button act copy-clip ?disabled=${this.locked
|
|
206
|
-
@click=${() =>
|
|
253
|
+
<button act copy-clip ?disabled=${this.locked} title=${i18next.t(grouped ? 'figure.text.copy-clip-explained' : 'figure.text.copy-clip-makes-group')}
|
|
254
|
+
@click=${() => {
|
|
255
|
+
/* The copy is what is keyed next: the pose bar turns to it. */
|
|
256
|
+
this.active = freeClipNameOf(this.asset!, clip.name)
|
|
257
|
+
this.edits(copyClipEdits(this.asset!, clip.name, i18next.t('figure.label.motion-group-named')))
|
|
258
|
+
}}>
|
|
207
259
|
<md-icon>content_copy</md-icon>${i18next.t('figure.button.copy-clip')}
|
|
208
260
|
</button>
|
|
209
261
|
<div row>
|
|
@@ -236,6 +288,35 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
236
288
|
</section>`
|
|
237
289
|
}
|
|
238
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The clip being keyed -- the one last scrubbed or chosen here -- with its moment and 「현재 자세 기록」, right under the
|
|
293
|
+
* controls: type the controls, then record, without scrolling to the clip.
|
|
294
|
+
*/
|
|
295
|
+
private renderPoseBar(clips: Clip[]) {
|
|
296
|
+
const clip = clips.find(c => c.name === this.active) ?? clips[0]!
|
|
297
|
+
const at = Math.min(this.at[clip.name] ?? 0, clip.duration)
|
|
298
|
+
return html`<div pose-bar>
|
|
299
|
+
${clips.length > 1
|
|
300
|
+
? html`<span clip-choices role="group" aria-label=${i18next.t('figure.label.clips')}>
|
|
301
|
+
${clips.map(c => html`<button type="button" aria-pressed=${String(c === clip)} ?disabled=${this.locked} @click=${() => this.scrub(c, Math.min(this.at[c.name] ?? 0, c.duration))}>${c.name}</button>`)}
|
|
302
|
+
</span>`
|
|
303
|
+
: html`<strong>${clip.name}</strong>`}
|
|
304
|
+
<label at>
|
|
305
|
+
<input
|
|
306
|
+
type="number"
|
|
307
|
+
min="0"
|
|
308
|
+
max=${clip.duration}
|
|
309
|
+
step="any"
|
|
310
|
+
.value=${String(Math.round(at * 1000) / 1000)}
|
|
311
|
+
?disabled=${this.locked}
|
|
312
|
+
aria-label=${i18next.t('figure.label.clip-time')}
|
|
313
|
+
@change=${(e: Event) => this.scrub(clip, Math.min(Math.max((e.target as HTMLInputElement).valueAsNumber, 0), clip.duration))}
|
|
314
|
+
/>s
|
|
315
|
+
</label>
|
|
316
|
+
${this.renderRemember(clip, at)}
|
|
317
|
+
</div>`
|
|
318
|
+
}
|
|
319
|
+
|
|
239
320
|
/** 「이 자세 기억하기」: every control moved since the clip's pose was last shown or kept, as keys at this moment, one step. */
|
|
240
321
|
private renderRemember(clip: Clip, at: number) {
|
|
241
322
|
const { edits, skipped } = rememberedPoseEdits(this.asset!, clip.name, at, this.values, this.base[clip.name] ?? this.values)
|
|
@@ -380,9 +461,11 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
380
461
|
const taken = !joining && clips.some(c => c.name === name)
|
|
381
462
|
const ready = !!name && !taken && duration > 0 && !!input
|
|
382
463
|
const join = !joining && input && name ? newClipEdits(asset, stateId!, name, duration, []).join : undefined
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
464
|
+
/* A track that holds the pose now is the start of a move from pose to pose: smooth (review 2026-11-11, a walk's nine
|
|
465
|
+
tracks each changed by hand). The range preset sweeps at a steady rate, as before. */
|
|
466
|
+
const add = (keys: ClipKey[], interpolation?: 'smooth') => {
|
|
467
|
+
if (joining) this.edit({ kind: 'set-driver', state: stateId, clip: name, keys, ...(interpolation ? { interpolation } : {}) })
|
|
468
|
+
else this.edits(newClipEdits(asset, stateId!, name, duration, keys, interpolation).edits)
|
|
386
469
|
/* The name was this clip's; the next new clip is named again. */
|
|
387
470
|
this.adding = { ...a, name: '', state: '' }
|
|
388
471
|
}
|
|
@@ -409,7 +492,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
409
492
|
: join?.kind === 'group'
|
|
410
493
|
? html`<p hint track-join>${i18next.t('figure.text.track-joins-group', { name, group: join.label })}</p>`
|
|
411
494
|
: nothing}
|
|
412
|
-
<button act ?disabled=${this.locked || !ready} @click=${() => add(newTrackKeys(input!, this.values[stateId!], duration))}>
|
|
495
|
+
<button act ?disabled=${this.locked || !ready} @click=${() => add(newTrackKeys(input!, this.values[stateId!], duration), 'smooth')}>
|
|
413
496
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-track')}
|
|
414
497
|
</button>
|
|
415
498
|
<button act range-track ?disabled=${this.locked || !ready} title=${i18next.t('figure.text.add-track-range-explained')}
|
|
@@ -515,12 +598,22 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
515
598
|
</section>`
|
|
516
599
|
}
|
|
517
600
|
|
|
601
|
+
/** A clip renamed: the pose bar, its moment and its pose stay with it under the new name. */
|
|
602
|
+
private rename(clip: Clip, name: string) {
|
|
603
|
+
const moved = <T,>(by: Record<string, T>) => (clip.name in by ? { ...by, [name]: by[clip.name]! } : by)
|
|
604
|
+
this.at = moved(this.at)
|
|
605
|
+
this.base = moved(this.base)
|
|
606
|
+
if (this.active === clip.name) this.active = name
|
|
607
|
+
this.edit({ kind: 'set-clip', clip: clip.name, name })
|
|
608
|
+
}
|
|
609
|
+
|
|
518
610
|
/** Show the clip's pose at a moment: one batch of input changes, not saved as the default. */
|
|
519
611
|
/** Show the clip's pose at a moment. While the bar is dragged the pose is live (drawn, not recorded); the release
|
|
520
612
|
* applies it as one batch of input changes. */
|
|
521
613
|
private scrub(clip: Clip, seconds: number, live = false) {
|
|
522
614
|
if (!this.asset || !Number.isFinite(seconds)) return
|
|
523
615
|
this.at = { ...this.at, [clip.name]: seconds }
|
|
616
|
+
this.active = clip.name
|
|
524
617
|
const changes = poseAt(this.asset, clip.name, seconds)
|
|
525
618
|
if (!changes.length) return
|
|
526
619
|
if (live) {
|
|
@@ -89,7 +89,28 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
89
89
|
}
|
|
90
90
|
details[fasten-detail] > div[form] {
|
|
91
91
|
margin-top: 8px;
|
|
92
|
-
max-width:
|
|
92
|
+
max-width: 420px;
|
|
93
|
+
}
|
|
94
|
+
/* Where across the face, as buttons: a list the system draws is not seen in a recording, nor reached by a key in every browser. */
|
|
95
|
+
div[align-choices] {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
gap: 4px;
|
|
99
|
+
}
|
|
100
|
+
div[align-choices] button {
|
|
101
|
+
font: inherit;
|
|
102
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
103
|
+
padding: 3px 8px;
|
|
104
|
+
border-radius: 12px;
|
|
105
|
+
border: 1px solid var(--md-sys-color-outline-variant, #c4c7c5);
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: inherit;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
}
|
|
110
|
+
div[align-choices] button[aria-pressed='true'] {
|
|
111
|
+
border-color: var(--md-sys-color-primary, #0d8f86);
|
|
112
|
+
background: var(--md-sys-color-primary-container, #cdeeea);
|
|
113
|
+
font-weight: 600;
|
|
93
114
|
}
|
|
94
115
|
span[fasten-summary] {
|
|
95
116
|
opacity: 0.8;
|
|
@@ -253,7 +274,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
253
274
|
<summary>
|
|
254
275
|
${i18next.t('figure.label.fasten-detail')}${detail.length ? html` · <span fasten-summary>${detail.join(' · ')}</span>` : nothing}
|
|
255
276
|
</summary>
|
|
256
|
-
${plan.alignRows.length ? this.renderAlignment(plan.alignRows) : nothing}
|
|
277
|
+
${(plan.alignAxes ?? plan.alignRows).length ? this.renderAlignment(plan.alignAxes ?? plan.alignRows) : nothing}
|
|
257
278
|
<div form>
|
|
258
279
|
<span>${i18next.t('figure.label.gap')}</span>
|
|
259
280
|
<input
|
|
@@ -384,34 +405,24 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
384
405
|
: 'centre'
|
|
385
406
|
: (f.align[axis] ?? 'centre')
|
|
386
407
|
return html`<span>${i18next.t('figure.label.align-' + axis)}</span>
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
edges
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
407
|
-
${
|
|
408
|
-
this.revision && hasInstanceSize(this.revision.asset, axis)
|
|
409
|
-
? html`<option value="share" ?selected=${how === 'share'}>
|
|
410
|
-
${i18next.t('figure.label.align-share')}
|
|
411
|
-
</option>`
|
|
412
|
-
: nothing
|
|
413
|
-
}
|
|
414
|
-
</select>
|
|
408
|
+
<div align-choices role="group" aria-label=${i18next.t('figure.label.align-' + axis)}>
|
|
409
|
+
${[
|
|
410
|
+
{ value: 'centre' as Alignment, words: i18next.t('figure.label.align-centre'), shown: true },
|
|
411
|
+
{ value: 'min' as Alignment, words: i18next.t(`figure.label.align-${axis}-min`), shown: edges },
|
|
412
|
+
{ value: 'max' as Alignment, words: i18next.t(`figure.label.align-${axis}-max`), shown: edges },
|
|
413
|
+
{
|
|
414
|
+
value: 'input' as Alignment,
|
|
415
|
+
words: f.alignInput?.[axis] ? i18next.t('figure.label.align-input', { field: fieldWordsOf(f.alignInput[axis]!, key => i18next.t(key)) }) : '',
|
|
416
|
+
shown: !!f.alignInput?.[axis]
|
|
417
|
+
},
|
|
418
|
+
{ value: 'mm' as Alignment, words: i18next.t('figure.label.align-mm'), shown: true },
|
|
419
|
+
{ value: 'share' as Alignment, words: i18next.t('figure.label.align-share'), shown: !!this.revision && hasInstanceSize(this.revision.asset, axis) }
|
|
420
|
+
]
|
|
421
|
+
.filter(choice => choice.shown)
|
|
422
|
+
.map(
|
|
423
|
+
choice => html`<button type="button" aria-pressed=${String(how === choice.value)} @click=${() => set(axis, choice.value)}>${choice.words}</button>`
|
|
424
|
+
)}
|
|
425
|
+
</div>
|
|
415
426
|
${
|
|
416
427
|
how === 'mm' || how === 'share'
|
|
417
428
|
? html`<span></span
|
|
@@ -6,6 +6,7 @@ import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-
|
|
|
6
6
|
import { figureAroundNormalOf, figureAroundOf, figureCaughtAngleOf, figureSurfaceAnglesOf, figureSurfacePlaceOf } from '@hatiolab/figure-model'
|
|
7
7
|
import type { FigureFace, FigureSurfacePlace } from '@hatiolab/figure-model'
|
|
8
8
|
|
|
9
|
+
import { focusOnRender } from './focus-on-render.js'
|
|
9
10
|
import { alignmentAt, drawingSpanOf, fastenLabelsOf, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
10
11
|
import type { Alignment } from './links.js'
|
|
11
12
|
|
|
@@ -432,7 +433,8 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
432
433
|
this.change({ axis: a, how: 'mm', mm: Math.round(value) })
|
|
433
434
|
this.released()
|
|
434
435
|
}}
|
|
435
|
-
@blur=${() => (this.typing = undefined)}
|
|
436
|
+
@blur=${() => (this.typing = undefined)}
|
|
437
|
+
${focusOnRender()} />`
|
|
436
438
|
}
|
|
437
439
|
|
|
438
440
|
private grab(ev: PointerEvent, at: Record<Axis, number>, caught: Partial<Record<Axis, string>>) {
|
|
@@ -7,7 +7,7 @@ import { CommonPopupStyles, FormFieldStyles } from '@operato/styles'
|
|
|
7
7
|
import '@material/web/button/filled-button.js'
|
|
8
8
|
import '@material/web/button/text-button.js'
|
|
9
9
|
|
|
10
|
-
import { firstSaveRefusalOf, offeredTypeOf, releasableType, typeOf, type FirstSave } from './first-save.js'
|
|
10
|
+
import { firstSaveRefusalOf, offeredTypeOf, releasableType, typeOf, typingTypeOf, type FirstSave } from './first-save.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* The first save of a new figure: its type and its name, asked once, in the suite's popup look.
|
|
@@ -97,13 +97,13 @@ export class FigureFirstSave extends LitElement {
|
|
|
97
97
|
.value=${this.type}
|
|
98
98
|
placeholder=${i18next.t('figure.text.figure-type-example')}
|
|
99
99
|
@input=${(e: Event) => {
|
|
100
|
-
this.type =
|
|
100
|
+
this.type = typingTypeOf((e.target as HTMLInputElement).value)
|
|
101
101
|
;(e.target as HTMLInputElement).value = this.type
|
|
102
102
|
this.refusal = null
|
|
103
103
|
}}
|
|
104
104
|
@keydown=${this.onKey}
|
|
105
105
|
/>
|
|
106
|
-
${this.type && !releasableType(this.type)
|
|
106
|
+
${typeOf(this.type) && !releasableType(typeOf(this.type))
|
|
107
107
|
? html`<div class="note">${i18next.t('figure.text.type-name-will-not-release')}</div>`
|
|
108
108
|
: this.suggested && this.type === this.suggested
|
|
109
109
|
? html`<div class="note">${i18next.t('figure.text.identifier-suggested')}</div>`
|
|
@@ -132,6 +132,7 @@ export class FigureFirstSave extends LitElement {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
private confirm = () => {
|
|
135
|
+
this.type = typeOf(this.type)
|
|
135
136
|
this.refusal = firstSaveRefusalOf(this.type, this.taken)
|
|
136
137
|
if (this.refusal) return
|
|
137
138
|
const namespace = this.namespaces.length > 1 ? this.chosenNamespace : undefined
|