@things-factory/figure-ui 10.1.129 → 10.1.142
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/align.ts +24 -18
- package/client/modeller/editor.ts +165 -253
- package/client/modeller/fasten-geometry.ts +12 -9
- package/client/modeller/figure-facts.ts +12 -14
- package/client/modeller/figure-links.ts +16 -12
- package/client/modeller/figure-preview.ts +13 -4
- package/client/modeller/figure-properties.ts +6 -5
- package/client/modeller/figure-verdict.ts +14 -8
- package/client/modeller/joint-fan.ts +15 -17
- package/client/modeller/links.ts +17 -23
- package/client/modeller/live.ts +6 -3
- package/client/modeller/motion-ends.ts +6 -7
- package/client/modeller/occupancy.ts +2 -3
- package/client/modeller/product-session.ts +15 -0
- package/client/modeller/revision.ts +66 -0
- package/client/pages/figure-modeller-page.ts +5 -18
- package/dist-client/modeller/align.d.ts +12 -7
- package/dist-client/modeller/align.js +21 -17
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +8 -34
- package/dist-client/modeller/editor.js +175 -250
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +3 -2
- package/dist-client/modeller/fasten-geometry.js +11 -9
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-facts.d.ts +9 -1
- package/dist-client/modeller/figure-facts.js +11 -15
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -5
- package/dist-client/modeller/figure-links.js +14 -13
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +5 -2
- package/dist-client/modeller/figure-preview.js +13 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +3 -2
- package/dist-client/modeller/figure-properties.js +6 -6
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +4 -2
- package/dist-client/modeller/figure-verdict.js +12 -9
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/joint-fan.d.ts +8 -4
- package/dist-client/modeller/joint-fan.js +15 -16
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +6 -5
- package/dist-client/modeller/links.js +17 -24
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +3 -2
- package/dist-client/modeller/live.js +7 -3
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +2 -2
- package/dist-client/modeller/motion-ends.js +5 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/occupancy.d.ts +2 -2
- package/dist-client/modeller/occupancy.js +2 -3
- package/dist-client/modeller/occupancy.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +44 -39
- package/dist-client/modeller/product-session.js +17 -0
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/revision.d.ts +37 -0
- package/dist-client/modeller/revision.js +39 -0
- package/dist-client/modeller/revision.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +0 -6
- package/dist-client/pages/figure-modeller-page.js +6 -14
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +14 -12
- package/test/browser/modeller-page.js +52 -4
- package/test/browser/modeller-revision.check.mjs +144 -0
- package/test/command-preview.test.ts +9 -8
- package/test/fasten-drawing.test.ts +1 -1
- package/test/fasten-geometry.test.ts +10 -9
- package/test/fields.test.ts +1 -1
- package/test/first-minute.test.ts +1 -1
- package/test/gizmo.test.ts +1 -1
- package/test/joint-fan.test.ts +30 -14
- package/test/joint-frames.test.ts +9 -8
- package/test/links.test.ts +13 -12
- package/test/live.test.ts +3 -3
- package/test/parts-wiring.test.ts +1 -1
- package/test/properties.test.ts +6 -5
- package/test/release-panel.test.ts +3 -3
- package/test/resize-with-placement.test.ts +8 -6
- package/test/revision-of.ts +22 -0
- package/test/revision.test.ts +172 -0
- package/test/section-editor.test.ts +4 -3
- package/test/section-panel.test.ts +5 -4
- package/test/verdict.test.ts +1 -1
- package/client/modeller/evaluated.ts +0 -47
- package/dist-client/modeller/evaluated.d.ts +0 -12
- package/dist-client/modeller/evaluated.js +0 -42
- package/dist-client/modeller/evaluated.js.map +0 -1
- package/dist-client/tsconfig.scratch.tsbuildinfo +0 -1
- package/test/evaluated.test.ts +0 -40
- package/test/mode-switch.test.ts +0 -43
|
@@ -3,6 +3,7 @@ import type { FigureAsset, FigureFace } from '@hatiolab/figure-model'
|
|
|
3
3
|
|
|
4
4
|
import { boxOf } from './align.js'
|
|
5
5
|
import { alignAxesOf, facesOf, faceAxisOf, hasInstanceSize, mountingPlaneOf, type Alignment } from './links.js'
|
|
6
|
+
import type { FigureRevision } from './revision.js'
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
9
|
* What the 붙이기 drawing reads (user 2026-10-01): the face a part is fastened to, seen from above, and where the part
|
|
@@ -19,11 +20,11 @@ export type FastenTarget = { part: string; face: string } | 'plane'
|
|
|
19
20
|
const OPPOSITE: Record<FigureFace, FigureFace> = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' }
|
|
20
21
|
const NORMAL: Record<FigureFace, [Axis, 1 | -1]> = { left: ['x', -1], right: ['x', 1], bottom: ['y', -1], top: ['y', 1], back: ['z', -1], front: ['z', 1] }
|
|
21
22
|
|
|
22
|
-
/** The world direction a part's face looks along now, turns and all; by its name when the face's outline is not read. */
|
|
23
|
-
function directionOf(
|
|
23
|
+
/** The world direction a part's face looks along now, as drawn, turns and all; by its name when the face's outline is not read. */
|
|
24
|
+
function directionOf(revision: FigureRevision, part: string, face: string): [Axis, 1 | -1] | undefined {
|
|
24
25
|
const area = (() => {
|
|
25
26
|
try {
|
|
26
|
-
return figurePartFaceOf(asset, part, face)
|
|
27
|
+
return figurePartFaceOf(revision.asset, part, face, revision.inputs)
|
|
27
28
|
} catch {
|
|
28
29
|
return undefined
|
|
29
30
|
}
|
|
@@ -40,19 +41,20 @@ function directionOf(asset: FigureAsset, part: string, face: string): [Axis, 1 |
|
|
|
40
41
|
* centre. Read from where each face looks now, so a part standing turned offers the face that is really there.
|
|
41
42
|
* Undefined when the part has no such face.
|
|
42
43
|
*/
|
|
43
|
-
export function myFaceFor(
|
|
44
|
+
export function myFaceFor(revision: FigureRevision, part: string, target: FastenTarget, mode: FastenMode): FigureFace | 'centre' | undefined {
|
|
45
|
+
const asset = revision.asset
|
|
44
46
|
if (mode === 'centre') return target === 'plane' ? undefined : 'centre'
|
|
45
47
|
const want: [Axis, 1 | -1] | undefined =
|
|
46
48
|
target === 'plane'
|
|
47
49
|
? /* The plane is met from above on a floor, from in front on a wall. */ [mountingPlaneOf(asset).normal, -1]
|
|
48
50
|
: (() => {
|
|
49
|
-
const d = directionOf(
|
|
51
|
+
const d = directionOf(revision, target.part, target.face)
|
|
50
52
|
return d && [d[0], (mode === 'meet' ? -d[1] : d[1]) as 1 | -1]
|
|
51
53
|
})()
|
|
52
54
|
if (!want) return undefined
|
|
53
55
|
const mine = facesOf(asset, part).filter((f): f is FigureFace => f in NORMAL)
|
|
54
56
|
const hit = mine.find(f => {
|
|
55
|
-
const d = directionOf(
|
|
57
|
+
const d = directionOf(revision, part, f)
|
|
56
58
|
return d && d[0] === want[0] && d[1] === want[1]
|
|
57
59
|
})
|
|
58
60
|
return hit ?? (target !== 'plane' && mode === 'meet' && target.face in OPPOSITE ? OPPOSITE[target.face as FigureFace] : undefined)
|
|
@@ -72,17 +74,18 @@ export interface FaceView {
|
|
|
72
74
|
edges: Record<Axis, boolean>
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
export function faceViewOf(
|
|
77
|
+
export function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget): FaceView | undefined {
|
|
78
|
+
const asset = revision.asset
|
|
76
79
|
const across = alignAxesOf(asset, part, target)
|
|
77
80
|
if (across.length !== 2) return undefined
|
|
78
|
-
const box = boxOf(
|
|
81
|
+
const box = boxOf(revision, part)
|
|
79
82
|
if (!box) return undefined
|
|
80
83
|
const axes = [across[0]!.axis, across[1]!.axis] as [Axis, Axis]
|
|
81
84
|
const face: Partial<Record<Axis, number>> = {}
|
|
82
85
|
if (target === 'plane') {
|
|
83
86
|
for (const a of axes) if (hasInstanceSize(asset, a)) face[a] = asset.designInputs[`size.${a}`]!
|
|
84
87
|
} else {
|
|
85
|
-
const theirs = boxOf(
|
|
88
|
+
const theirs = boxOf(revision, target.part)
|
|
86
89
|
const normal = faceAxisOf(asset, target.part, target.face)
|
|
87
90
|
for (const a of axes) if (theirs && a !== normal) face[a] = theirs.extent[a]
|
|
88
91
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { compileFigureAsset, parseFigureAsset, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'
|
|
2
1
|
import type { FigureAsset, FigureFace } from '@hatiolab/figure-model'
|
|
3
2
|
|
|
3
|
+
import { boxesOf } from './align.js'
|
|
4
|
+
import { derivedOf, type FigureRevision } from './revision.js'
|
|
5
|
+
|
|
4
6
|
/*
|
|
5
7
|
* What the properties tab reads, kept apart from the element so it can be tested without a browser.
|
|
6
8
|
*/
|
|
@@ -29,16 +31,11 @@ export interface FigureFigureFacts {
|
|
|
29
31
|
* records the face only as that node, so this is where the screen learns which one is chosen. Parts are the placed
|
|
30
32
|
* bodies (`place@1`), in the order the model lists them.
|
|
31
33
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/* The panel renders more often than the figure changes; the same text says the same facts. */
|
|
35
|
-
if (lastFacts?.source === source) return lastFacts.facts
|
|
36
|
-
const facts = figureFactsOf(source)
|
|
37
|
-
lastFacts = { source, facts }
|
|
38
|
-
return facts
|
|
34
|
+
export function figureFigureFacts(revision: FigureRevision): FigureFigureFacts {
|
|
35
|
+
return derivedOf(revision, 'figure-facts', () => figureFactsOf(revision))
|
|
39
36
|
}
|
|
40
|
-
function figureFactsOf(
|
|
41
|
-
const asset =
|
|
37
|
+
function figureFactsOf(revision: FigureRevision): FigureFigureFacts {
|
|
38
|
+
const asset = revision.asset as FigureAsset & { document: { model: { nodes: Array<{ id: string; op: string }> } } }
|
|
42
39
|
const parts = asset.document.model.nodes.filter(node => node.op === 'place@1').map(node => node.id)
|
|
43
40
|
const feature = (asset.occupancy?.contact as { surface?: { feature?: string } } | undefined)?.surface?.feature
|
|
44
41
|
const found = feature?.match(/^(.+)\.mount\.(left|right|bottom|top|back|front)\.value$/)
|
|
@@ -48,16 +45,17 @@ function figureFactsOf(source: string): FigureFigureFacts {
|
|
|
48
45
|
declared: !!asset.occupancy,
|
|
49
46
|
placement: asset.placement as Placement | undefined,
|
|
50
47
|
mounting: found ? { part: found[1], face: found[2] as FigureFace } : undefined,
|
|
51
|
-
suggested: suggestedMountOf(
|
|
48
|
+
suggested: suggestedMountOf(revision, parts)
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
/* As the parts are drawn now. */
|
|
53
|
+
function suggestedMountOf(revision: FigureRevision, parts: string[]): { part: string; face: FigureFace } | undefined {
|
|
54
|
+
const placement = (revision.asset.placement ?? 'floor') as Placement
|
|
57
55
|
if (placement === 'center' || !parts.length) return undefined
|
|
58
56
|
let boxes
|
|
59
57
|
try {
|
|
60
|
-
boxes =
|
|
58
|
+
boxes = boxesOf(revision).filter(b => parts.includes(b.part))
|
|
61
59
|
} catch {
|
|
62
60
|
return undefined
|
|
63
61
|
}
|
|
@@ -55,6 +55,8 @@ import { SidePanelStyles } from './side-panel-styles.js'
|
|
|
55
55
|
import './figure-fasten-drawing.js'
|
|
56
56
|
import type { FastenChange } from './figure-fasten-drawing.js'
|
|
57
57
|
import { faceViewOf, modeOf, myFaceFor, turnsInPlace, type FastenMode } from './fasten-geometry.js'
|
|
58
|
+
import { boxOf } from './align.js'
|
|
59
|
+
import type { FigureRevision } from './revision.js'
|
|
58
60
|
import { HelpToggles } from './help.js'
|
|
59
61
|
import { jointAxisWord } from './joint-axis.js'
|
|
60
62
|
|
|
@@ -149,12 +151,13 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
149
151
|
}
|
|
150
152
|
`]
|
|
151
153
|
|
|
152
|
-
/** The
|
|
153
|
-
@property({ attribute: false })
|
|
154
|
+
/** The figure at the version on screen. */
|
|
155
|
+
@property({ attribute: false }) revision?: FigureRevision
|
|
156
|
+
private get asset(): FigureAsset | undefined {
|
|
157
|
+
return this.revision?.asset
|
|
158
|
+
}
|
|
154
159
|
/** The selected part. */
|
|
155
160
|
@property() partId = ''
|
|
156
|
-
/** The part's own extent along its axes now, in mm — what a dimension is fixed at when it stops following. */
|
|
157
|
-
@property({ attribute: false }) dims?: [number, number, number]
|
|
158
161
|
@property({ type: Boolean }) locked = false
|
|
159
162
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
160
163
|
@property() section: 'shape' | 'places' | 'motion' = 'shape'
|
|
@@ -201,7 +204,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
201
204
|
if (this.asset && this.partId && this.partId !== this.loadedFor) {
|
|
202
205
|
this.loadedFor = this.partId
|
|
203
206
|
/* A part's own fastening is not another part's choices: the form opens on it (walk 1, 2026-09-26). */
|
|
204
|
-
const own = this.
|
|
207
|
+
const own = this.revision && this.partId ? fasteningOf(this.revision, this.partId) : null
|
|
205
208
|
this.fasten = own ? ({ ...freshFasten(), ...own } as ReturnType<typeof freshFasten>) : freshFasten()
|
|
206
209
|
this.motion = freshMotion()
|
|
207
210
|
this.place = freshPlace()
|
|
@@ -339,7 +342,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
339
342
|
const now = followOf(asset, this.partId, axis)
|
|
340
343
|
const following = now.of === 'instance' && now.axis === axis
|
|
341
344
|
const custom = now.of !== 'own' && !following
|
|
342
|
-
const mm = measureNow(
|
|
345
|
+
const mm = measureNow(this.revision!, this.partId, axis)
|
|
343
346
|
return html`<div row>
|
|
344
347
|
<b>${word(axis)}</b>
|
|
345
348
|
<span measure>${mm === undefined ? '—' : `${Math.round(mm)} mm`}</span>
|
|
@@ -422,7 +425,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
422
425
|
}
|
|
423
426
|
|
|
424
427
|
private async openFastening(asset: FigureAsset) {
|
|
425
|
-
const own = fasteningOf(
|
|
428
|
+
const own = fasteningOf(this.revision!, this.partId)
|
|
426
429
|
this.fasten = own ? ({ ...freshFasten(), ...own } as ReturnType<typeof freshFasten>) : freshFasten()
|
|
427
430
|
this.spotlit = true
|
|
428
431
|
await this.updateComplete
|
|
@@ -477,7 +480,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
477
480
|
private follow(asset: FigureAsset, axis: Axis, choice: string, now: Follow) {
|
|
478
481
|
const following = now.of !== 'own'
|
|
479
482
|
/* The measure as it stands: a dimension stops following at it, and a share is taken of it. */
|
|
480
|
-
|
|
483
|
+
/* The part's own extent along the axis now, as drawn: what a dimension is fixed at when it stops following. */
|
|
484
|
+
const current = dimensionNow(this.revision!, this.partId, axis) ?? boxOf(this.revision!, this.partId)?.dims[AXES.indexOf(axis)] ?? 100
|
|
481
485
|
if (choice === 'own') {
|
|
482
486
|
if (!following) return
|
|
483
487
|
this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm: Math.max(1, Math.round(current)) })
|
|
@@ -524,13 +528,13 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
524
528
|
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
525
529
|
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]!
|
|
526
530
|
const aim = f.toPlane ? ('plane' as const) : f.target ? { part: f.target, face: targetFace } : undefined
|
|
527
|
-
this.fasten = { ...f, ...(aim ? placeKeptOf(
|
|
531
|
+
this.fasten = { ...f, ...(aim ? placeKeptOf(this.revision!, this.partId, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) }
|
|
528
532
|
}
|
|
529
533
|
|
|
530
534
|
private renderFastening(asset: FigureAsset) {
|
|
531
535
|
const held = heldBy(asset, this.partId)
|
|
532
536
|
/* Placed by an origin (ADR-0107 decision 4): attach writes faces, so the form would move it off its origin. Said, and taken off only. */
|
|
533
|
-
if (fasteningOf(
|
|
537
|
+
if (fasteningOf(this.revision!, this.partId)?.atOrigin) {
|
|
534
538
|
this.fastenCandidate = undefined
|
|
535
539
|
return html`<div fastened fasten-at-origin>
|
|
536
540
|
<p>${this.heldWords(asset)}</p>
|
|
@@ -559,7 +563,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
559
563
|
const aim = f.toPlane ? ('plane' as const) : { part: f.target, face: targetFace }
|
|
560
564
|
/* The part's own face is not chosen: the target's face and how give it (user 2026-10-01; the kernel takes no other). */
|
|
561
565
|
const mode: FastenMode = f.toPlane ? 'meet' : modeOf(f.face, f.facing)
|
|
562
|
-
const derived = f.toPlane || f.target ? myFaceFor(
|
|
566
|
+
const derived = f.toPlane || f.target ? myFaceFor(this.revision!, this.partId, aim, mode) : undefined
|
|
563
567
|
const myFace = derived ?? (mineFaces.includes(f.face) ? f.face : mineFaces[0]!)
|
|
564
568
|
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist, f.alignInput) : undefined
|
|
565
569
|
const action =
|
|
@@ -587,7 +591,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
587
591
|
...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
|
|
588
592
|
]
|
|
589
593
|
/* Across the face, a row only where the drawing cannot set it: no drawing of the face, or a share or a field to follow. */
|
|
590
|
-
const view = f.toPlane || f.target ? faceViewOf(
|
|
594
|
+
const view = f.toPlane || f.target ? faceViewOf(this.revision!, this.partId, aim) : undefined
|
|
591
595
|
const alignRows = (f.toPlane || f.target ? alignAxesOf(asset, this.partId, aim, !!f.twist) : []).filter(
|
|
592
596
|
({ axis }) => !view || !view.axes.includes(axis) || !!f.alignInput?.[axis] || hasInstanceSize(asset, axis)
|
|
593
597
|
)
|
|
@@ -8,6 +8,7 @@ import { create, registerFigure, unregisterFigure } from '@hatiolab/things-scene
|
|
|
8
8
|
|
|
9
9
|
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'
|
|
10
10
|
import { dialShape } from './angle-dial.js'
|
|
11
|
+
import { derivedOf, type FigureRevision } from './revision.js'
|
|
11
12
|
import { authoredSizeOf, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, setupPartOf } from './preview-model.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -170,7 +171,15 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
170
171
|
`
|
|
171
172
|
|
|
172
173
|
/** The figure being authored, as the editor holds it now. */
|
|
173
|
-
@property({ attribute: false })
|
|
174
|
+
@property({ attribute: false }) revision?: FigureRevision
|
|
175
|
+
private get asset(): FigureAsset | undefined {
|
|
176
|
+
return this.revision?.asset
|
|
177
|
+
}
|
|
178
|
+
/** The size the figure was made at, once per version: without a declared size it is measured by evaluating the figure. */
|
|
179
|
+
private authoredSize(): { x: number; y: number; z: number } {
|
|
180
|
+
const shown = this.revision!
|
|
181
|
+
return derivedOf(shown, 'authored-size', () => authoredSizeOf(shown.asset))
|
|
182
|
+
}
|
|
174
183
|
/** Which figure: the type it is registered under here is this one's own, never a real type a board uses. */
|
|
175
184
|
@property() context = ''
|
|
176
185
|
/**
|
|
@@ -209,7 +218,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
209
218
|
const clips = asset ? [...new Set((asset.drivers ?? []).map(d => d.clip))] : []
|
|
210
219
|
let clampsByWay: ReturnType<typeof previewClampsOf> = {}
|
|
211
220
|
try {
|
|
212
|
-
clampsByWay = asset ? previewClampsOf(asset,
|
|
221
|
+
clampsByWay = asset ? previewClampsOf(asset, this.authoredSize()) : {}
|
|
213
222
|
} catch {
|
|
214
223
|
clampsByWay = {}
|
|
215
224
|
}
|
|
@@ -322,7 +331,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
322
331
|
this.standUp()
|
|
323
332
|
return
|
|
324
333
|
}
|
|
325
|
-
if (changed.has('
|
|
334
|
+
if (changed.has('revision') || changed.has('context')) {
|
|
326
335
|
this.standUp()
|
|
327
336
|
return
|
|
328
337
|
}
|
|
@@ -368,7 +377,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
368
377
|
try {
|
|
369
378
|
/* Registered as a board registers it, under a type of this preview's own: a real type's drawings stay as they are. */
|
|
370
379
|
const type = `figure-preview-${this.context || 'new'}`
|
|
371
|
-
const size =
|
|
380
|
+
const size = this.authoredSize()
|
|
372
381
|
/*
|
|
373
382
|
The figure changed under a scene that is standing: the same type is registered again and the content made again
|
|
374
383
|
inside the same 3D scene. Making a scene anew costs about a third of a second and its first drawing compiles the
|
|
@@ -10,6 +10,7 @@ import type { FigureAuthoringAction, FigureFace } from '@hatiolab/figure-model'
|
|
|
10
10
|
|
|
11
11
|
import { occupancyFromParts } from './occupancy.js'
|
|
12
12
|
import { tagsOf, figureFigureFacts, type Placement, type FigureFigureFacts } from './figure-facts.js'
|
|
13
|
+
import type { FigureRevision } from './revision.js'
|
|
13
14
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
14
15
|
import { ScrollbarStyles } from '@operato/styles'
|
|
15
16
|
|
|
@@ -58,8 +59,8 @@ export class FigureProperties extends localize(i18next)(LitElement) {
|
|
|
58
59
|
}
|
|
59
60
|
`]
|
|
60
61
|
|
|
61
|
-
/** The
|
|
62
|
-
@property({
|
|
62
|
+
/** The figure at the version on screen. */
|
|
63
|
+
@property({ attribute: false }) revision?: FigureRevision
|
|
63
64
|
/** The saved row, if any. Category and tags live on it. */
|
|
64
65
|
@property({ type: String }) figureId?: string
|
|
65
66
|
@property({ type: String }) figureCategory?: string
|
|
@@ -70,7 +71,7 @@ export class FigureProperties extends localize(i18next)(LitElement) {
|
|
|
70
71
|
|
|
71
72
|
private facts(): FigureFigureFacts | undefined {
|
|
72
73
|
try {
|
|
73
|
-
return this.
|
|
74
|
+
return this.revision ? figureFigureFacts(this.revision) : undefined
|
|
74
75
|
} catch {
|
|
75
76
|
/* A source the format refuses has no facts to show; the verdict tab says why. */
|
|
76
77
|
return undefined
|
|
@@ -252,8 +253,8 @@ export class FigureProperties extends localize(i18next)(LitElement) {
|
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
private declare(placement: Placement) {
|
|
255
|
-
if (!this.
|
|
256
|
-
this.command(occupancyFromParts(this.
|
|
256
|
+
if (!this.revision) return
|
|
257
|
+
this.command(occupancyFromParts(this.revision.asset, placement))
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
private command(action: FigureAuthoringAction) {
|
|
@@ -5,12 +5,13 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
|
5
5
|
|
|
6
6
|
import { i18next, localize } from '@operato/i18n'
|
|
7
7
|
import { ScrollbarStyles } from '@operato/styles'
|
|
8
|
-
import { LIMITS,
|
|
8
|
+
import { LIMITS, FIGURE_PER_INSTANCE, figureCostOf } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
import { inspectFigureDraft } from '../graphql/index.js'
|
|
11
11
|
import type { FigureInspection } from '../types.js'
|
|
12
12
|
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
13
13
|
import { MEASURED } from './scene-budget.js'
|
|
14
|
+
import type { FigureRevision } from './revision.js'
|
|
14
15
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
15
16
|
import { budgetOf, CURVE_SIZE, curveLayout, GRADE_SCALE, shortNumber, figureReportOf } from './maturity.js'
|
|
16
17
|
import type { FigureBudgetUse, FigureMaturityAxis, FigureReport } from './maturity.js'
|
|
@@ -378,8 +379,10 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
378
379
|
`
|
|
379
380
|
]
|
|
380
381
|
|
|
381
|
-
/** The
|
|
382
|
-
@property({
|
|
382
|
+
/** The figure at the version on screen. A version whose text is another is a new question. */
|
|
383
|
+
@property({ attribute: false }) revision?: FigureRevision
|
|
384
|
+
/* The text last judged: a save hands out the same version again as a new object, which asks nothing new. */
|
|
385
|
+
private judged?: string
|
|
383
386
|
/** The saved row, if any — the server reads its type and last release from it. */
|
|
384
387
|
@property({ type: String }) figureId?: string
|
|
385
388
|
/** Changing the type changes the verdict (a temporary type is refused), so it is a reason to ask again. */
|
|
@@ -408,19 +411,22 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
408
411
|
/** The report is measured once the figure has settled: it is read beside the work, and each measure compiles the figure several times. */
|
|
409
412
|
private measureTimer?: ReturnType<typeof setTimeout>
|
|
410
413
|
updated(changes: Map<string, unknown>) {
|
|
411
|
-
|
|
414
|
+
const text = changes.has('revision') ? this.revision?.text : this.judged
|
|
415
|
+
const changed = text !== this.judged
|
|
416
|
+
if (changed) {
|
|
417
|
+
this.judged = text
|
|
412
418
|
clearTimeout(this.measureTimer)
|
|
413
419
|
if (this.report === undefined && !this.measureTimer) this.measure()
|
|
414
420
|
else this.measureTimer = setTimeout(() => this.measure(), MEASURE_SETTLE_MS)
|
|
415
421
|
}
|
|
416
|
-
if (
|
|
422
|
+
if (changed || changes.has('figureId') || changes.has('figureType')) this.schedule()
|
|
417
423
|
}
|
|
418
424
|
|
|
419
425
|
private measure() {
|
|
420
426
|
let report: FigureReport | undefined
|
|
421
427
|
let bodies = 0
|
|
422
428
|
try {
|
|
423
|
-
const asset = this.
|
|
429
|
+
const asset = this.revision?.asset
|
|
424
430
|
const cost = asset ? figureCostOf(asset) : undefined
|
|
425
431
|
bodies = cost ? cost.groups + cost.hidden : 0
|
|
426
432
|
report = asset ? figureReportOf(asset) : undefined
|
|
@@ -436,7 +442,7 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
436
442
|
|
|
437
443
|
private schedule() {
|
|
438
444
|
clearTimeout(this.timer)
|
|
439
|
-
if (!this.
|
|
445
|
+
if (!this.revision) {
|
|
440
446
|
this.inspection = undefined
|
|
441
447
|
return
|
|
442
448
|
}
|
|
@@ -445,7 +451,7 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
445
451
|
|
|
446
452
|
private async ask() {
|
|
447
453
|
const question = ++this.asked
|
|
448
|
-
const source = this.
|
|
454
|
+
const source = this.revision!.text
|
|
449
455
|
this.asking = true
|
|
450
456
|
try {
|
|
451
457
|
const inspection = await inspectFigureDraft(source, this.figureId)
|
|
@@ -4,7 +4,7 @@ import type { FigureAsset } from '@hatiolab/figure-model'
|
|
|
4
4
|
import type { FigureRenderPart } from './renderer.js'
|
|
5
5
|
import { sectionShape } from './drawn-shapes.js'
|
|
6
6
|
import { jointAxisUnit } from './joint-axis.js'
|
|
7
|
-
import {
|
|
7
|
+
import { derivedOf, graphValuesOf, type FigureRevision } from './revision.js'
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
10
|
* Where the selected part turns, and how far -- a translucent fan on the editing board. An axis name says nothing about which way a part swings; on a six-axis arm the author went to the preview and
|
|
@@ -183,29 +183,32 @@ function turnOf(asset: FigureAsset, part: string, values: Record<string, unknown
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
|
-
* The fan for the part, as a body to stand on the board, or null when the part does not turn.
|
|
187
|
-
*
|
|
186
|
+
* The fan for the part, as a body to stand on the board, or null when the part does not turn. Drawn at the version as it
|
|
187
|
+
* is drawn; with a value in hand, `live` gives the values in hand and the bodies drawn with them, so the fan follows the
|
|
188
|
+
* arm as it is posed.
|
|
188
189
|
*/
|
|
189
|
-
export function jointFanOf(
|
|
190
|
+
export function jointFanOf(revision: FigureRevision, part: string, live?: { values: Record<string, number>; geometry: readonly unknown[] }): FigureRenderPart | null {
|
|
190
191
|
if (!part) return null
|
|
192
|
+
const asset = revision.asset
|
|
191
193
|
let graph: ReturnType<typeof compileFigureGraph>
|
|
192
194
|
try {
|
|
193
|
-
graph = compileFigureGraph(asset.document.model as never)
|
|
195
|
+
graph = derivedOf(revision, 'graph', () => compileFigureGraph(asset.document.model as never))
|
|
194
196
|
} catch {
|
|
195
197
|
return null
|
|
196
198
|
}
|
|
197
|
-
const inputs = { ...
|
|
199
|
+
const inputs = { ...revision.inputs, ...live?.values }
|
|
198
200
|
const at = (over: Record<string, number>) => graph.evaluate({ ...inputs, ...over }).values as Record<string, unknown>
|
|
201
|
+
const geometry = (live?.geometry ?? revision.geometry) as readonly { id: string }[]
|
|
199
202
|
let now: Record<string, unknown>
|
|
200
203
|
try {
|
|
201
|
-
now = at({})
|
|
204
|
+
now = live ? at({}) : (graphValuesOf(revision) as Record<string, unknown>)
|
|
202
205
|
} catch {
|
|
203
206
|
return null
|
|
204
207
|
}
|
|
205
208
|
let box: WorldBox | undefined
|
|
206
209
|
try {
|
|
207
210
|
box = figureWorldBoxesOf(
|
|
208
|
-
|
|
211
|
+
geometry.filter(g => g.id === part) as never,
|
|
209
212
|
figureWritersOf(asset)
|
|
210
213
|
)[0]
|
|
211
214
|
} catch {
|
|
@@ -225,7 +228,7 @@ export function jointFanOf(asset: FigureAsset, part: string, values: Record<stri
|
|
|
225
228
|
Sized from what the joint turns, not from the selected part: selecting the elbow housing or the forearm
|
|
226
229
|
shows the elbow's one fan, not a smaller one for the smaller part (user, 2026-09-24).
|
|
227
230
|
*/
|
|
228
|
-
const radius = Math.max(60, Math.round(Math.max(largestMeasureOf(
|
|
231
|
+
const radius = Math.max(60, Math.round(Math.max(largestMeasureOf(geometry, turn.turnedPart), Math.hypot(...inPlane)) * 0.6))
|
|
229
232
|
const edge = turn.pivot.map((v, i) => v + d[i]! * radius)
|
|
230
233
|
|
|
231
234
|
const { min, max } = turn.control
|
|
@@ -265,15 +268,10 @@ export function jointFanOf(asset: FigureAsset, part: string, values: Record<stri
|
|
|
265
268
|
} as unknown as FigureRenderPart
|
|
266
269
|
}
|
|
267
270
|
|
|
268
|
-
/**
|
|
269
|
-
function
|
|
270
|
-
return Object.fromEntries(Object.entries(values).filter(([id]) => id in of))
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** A part's largest own measure (a box side, a diameter, a length) at the inputs given; 0 when it cannot be read. */
|
|
274
|
-
function largestMeasureOf(asset: FigureAsset, part: string, inputs: Record<string, number>): number {
|
|
271
|
+
/** A part's largest own measure (a box side, a diameter, a length) as drawn; 0 when it cannot be read. */
|
|
272
|
+
function largestMeasureOf(geometry: readonly { id: string }[], part: string): number {
|
|
275
273
|
try {
|
|
276
|
-
const body =
|
|
274
|
+
const body = geometry.find(g => g.id === part) as { shape?: { dimensions?: Record<string, number> } } | undefined
|
|
277
275
|
const dims = Object.entries(body?.shape?.dimensions ?? {}).map(([k, v]) => (k.startsWith('radius') ? v * 2 : v))
|
|
278
276
|
return dims.length ? Math.max(...dims) : 0
|
|
279
277
|
} catch {
|
package/client/modeller/links.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset,
|
|
1
|
+
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, figureFollowOf, proposeFigureProportion, defaultInstanceSizeRange, foldInstanceSizeRange, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeLimitsOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureFollow, FigureSizeRange } from '@hatiolab/figure-model'
|
|
3
3
|
import type { FigureAsset, FigureBindingPlace, FigureFace, FigureJoint } from '@hatiolab/figure-model'
|
|
4
4
|
import { startingShapeKindOf } from './starting-shape.js'
|
|
5
|
-
import {
|
|
5
|
+
import { graphValuesOf, type FigureRevision } from './revision.js'
|
|
6
6
|
import { jointAxisToken, jointAxisVectorOf, type JointAxisVector } from './joint-axis.js'
|
|
7
7
|
|
|
8
8
|
/*
|
|
@@ -163,16 +163,15 @@ export function fieldWordsOf(id: string, say: (key: string) => string): string {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/** What a part's own measure on an axis is now (a box's side, a cylinder's radius or length), at the design as it stands. */
|
|
166
|
-
export function dimensionNow(
|
|
167
|
-
const m = modelOf(asset)
|
|
166
|
+
export function dimensionNow(revision: FigureRevision, part: string, axis: Axis): number | undefined {
|
|
167
|
+
const m = modelOf(revision.asset)
|
|
168
168
|
/* The argument the shape names for this axis (figure-model's provider), not its place among the arguments. */
|
|
169
169
|
const shape = m.nodes.find(n => n.id === `${part}.shape`)
|
|
170
170
|
const edit = shape && figureShapeProviderOf(shape as never)?.edit[axis]
|
|
171
171
|
const ref = edit && !('refused' in edit) ? shape!.args[edit.arg] : undefined
|
|
172
172
|
if (!ref) return undefined
|
|
173
173
|
try {
|
|
174
|
-
const
|
|
175
|
-
const value = values[ref]
|
|
174
|
+
const value = graphValuesOf(revision)[ref]
|
|
176
175
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
177
176
|
} catch {
|
|
178
177
|
return undefined
|
|
@@ -183,13 +182,12 @@ export function dimensionNow(asset: FigureAsset, part: string, axis: Axis): numb
|
|
|
183
182
|
* How long a part is along one axis now, as it is drawn: the shape's own measure through figure-model's provider (a
|
|
184
183
|
* cylinder's width is twice its radius), read after any rule it follows -- so a linked measure still says its size.
|
|
185
184
|
*/
|
|
186
|
-
export function measureNow(
|
|
187
|
-
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
185
|
+
export function measureNow(revision: FigureRevision, part: string, axis: Axis): number | undefined {
|
|
186
|
+
const shape = modelOf(revision.asset).nodes.find(n => n.id === `${part}.shape`)
|
|
188
187
|
const edit = shape && figureShapeProviderOf(shape as never)?.edit[axis]
|
|
189
188
|
if (!shape || !edit || 'refused' in edit) return undefined
|
|
190
189
|
try {
|
|
191
|
-
const
|
|
192
|
-
const value = values[shape.args[edit.arg]!]
|
|
190
|
+
const value = graphValuesOf(revision)[shape.args[edit.arg]!]
|
|
193
191
|
return typeof value === 'number' && Number.isFinite(value) ? value * edit.k : undefined
|
|
194
192
|
} catch {
|
|
195
193
|
return undefined
|
|
@@ -329,7 +327,7 @@ export function hangsFrom(asset: FigureAsset, part: string, from: string): boole
|
|
|
329
327
|
* from there broke its turning in place (walk 1, 2026-09-26). Null for a part fastened to nothing.
|
|
330
328
|
*/
|
|
331
329
|
export function fasteningOf(
|
|
332
|
-
|
|
330
|
+
revision: FigureRevision,
|
|
333
331
|
part: string
|
|
334
332
|
): {
|
|
335
333
|
face: string
|
|
@@ -344,7 +342,7 @@ export function fasteningOf(
|
|
|
344
342
|
/** Placed by a part's origin (ADR-0107 decision 4): no face to fasten by, and the form does not write it. */
|
|
345
343
|
atOrigin?: true
|
|
346
344
|
} | null {
|
|
347
|
-
const record = asset.joints?.find(j => j.body1.part === part)
|
|
345
|
+
const record = revision.asset.joints?.find(j => j.body1.part === part)
|
|
348
346
|
if (!record) return null
|
|
349
347
|
const toPlane = 'plane' in record.body0
|
|
350
348
|
const target = 'part' in record.body0 ? record.body0.part : ''
|
|
@@ -352,7 +350,7 @@ export function fasteningOf(
|
|
|
352
350
|
const atOrigin = !('plane' in record.body0) && (!('face' in record.body0) || !('face' in record.body1))
|
|
353
351
|
const twist = record.origin?.twist ?? 0
|
|
354
352
|
/* Where across the face: as the record says it, and where it says nothing, where the part stands now. */
|
|
355
|
-
const kept = placeKeptOf(
|
|
353
|
+
const kept = placeKeptOf(revision, part, toPlane ? 'plane' : { part: target, face: targetFace }, !!twist)
|
|
356
354
|
for (const [axis, how] of Object.entries(record.origin?.align ?? {}) as Array<[Axis, unknown]>) {
|
|
357
355
|
if (how === 'min' || how === 'max' || how === 'centre') {
|
|
358
356
|
kept.align[axis] = how
|
|
@@ -387,18 +385,15 @@ export function fasteningOf(
|
|
|
387
385
|
* its frame slid into it (2026-09-26). Left centred where it already is, and where the target stands turned.
|
|
388
386
|
*/
|
|
389
387
|
export function placeKeptOf(
|
|
390
|
-
|
|
388
|
+
revision: FigureRevision,
|
|
391
389
|
part: string,
|
|
392
390
|
target: { part: string; face: string } | 'plane',
|
|
393
391
|
twisted = false
|
|
394
392
|
): { align: Partial<Record<Axis, Alignment>>; alignMm: Partial<Record<Axis, number>>; alignInput: Partial<Record<Axis, string>> } {
|
|
395
393
|
const kept = { align: {} as Partial<Record<Axis, Alignment>>, alignMm: {} as Partial<Record<Axis, number>>, alignInput: {} as Partial<Record<Axis, string>> }
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} catch {
|
|
400
|
-
return kept
|
|
401
|
-
}
|
|
394
|
+
const asset = revision.asset
|
|
395
|
+
/* Where it stands as drawn: the pose the command that fastens it sees. */
|
|
396
|
+
const geometry = revision.geometry as unknown as Array<{ id: string; pose?: { r: number[][]; t: number[] } }>
|
|
402
397
|
const poseOf = (id: string) => geometry.find(g => g.id === id)?.pose
|
|
403
398
|
const mine = poseOf(part)
|
|
404
399
|
if (!mine) return kept
|
|
@@ -730,11 +725,10 @@ export function proportionToFollow(asset: FigureAsset): { count: number; kept: s
|
|
|
730
725
|
}
|
|
731
726
|
|
|
732
727
|
/** A value of the figure as it stands, by its graph reference. */
|
|
733
|
-
export function valueNow(
|
|
728
|
+
export function valueNow(revision: FigureRevision, ref: string | undefined): number | undefined {
|
|
734
729
|
if (!ref) return undefined
|
|
735
730
|
try {
|
|
736
|
-
const
|
|
737
|
-
const value = values[ref]
|
|
731
|
+
const value = graphValuesOf(revision)[ref]
|
|
738
732
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
739
733
|
} catch {
|
|
740
734
|
return undefined
|
package/client/modeller/live.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { applyFigureAuthoring, compileFigureAsset, editFigurePart } from '@hatio
|
|
|
2
2
|
import type { FigureAsset, FigureAuthoringAction, FigurePartEdit } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
import { drawableFigureParts, type FigureRenderPart } from './renderer.js'
|
|
5
|
+
import type { FigureRevision } from './revision.js'
|
|
5
6
|
|
|
6
7
|
/*
|
|
7
8
|
* The figure while a value is in hand: evaluated with the values being typed or dragged, drawn at once and written
|
|
@@ -62,6 +63,8 @@ export function livePartsOf(asset: FigureAsset, live: LiveValues): FigureRenderP
|
|
|
62
63
|
* point's coordinate before it was moved, a corner's radius before it was typed again) and would put it back.
|
|
63
64
|
*/
|
|
64
65
|
for (const id of Object.keys(design)) if (next.designInputs[id] !== asset.designInputs[id]) delete design[id]
|
|
66
|
+
/* A value the figure already holds changes nothing: a pose in hand alone is drawn from the figure as it is, not a copy. */
|
|
67
|
+
for (const id of Object.keys(design)) if (design[id] === next.designInputs[id]) delete design[id]
|
|
65
68
|
if (Object.keys(design).length) next = { ...next, designInputs: { ...next.designInputs, ...design } }
|
|
66
69
|
return drawableFigureParts(compileFigureAsset(next).evaluate(state).geometry)
|
|
67
70
|
} catch {
|
|
@@ -88,12 +91,12 @@ export function scrubbed(start: number, pixels: number, step: number): number {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/**
|
|
91
|
-
* The bodies to draw while a value is in hand, over every input as the session holds it now
|
|
94
|
+
* The bodies to draw while a value is in hand, over every input of the version as the session holds it now: the pose the
|
|
92
95
|
* author has already set with the other controls, and the design values. Drawing over the asset's saved defaults put
|
|
93
96
|
* every other joint back to its saved angle while one slider moved (user, 2026-09-24: the arm jumped on release).
|
|
94
97
|
*/
|
|
95
|
-
export function liveOverSession(
|
|
98
|
+
export function liveOverSession(revision: FigureRevision, live: LiveValues): FigureRenderPart[] | null {
|
|
96
99
|
if (!isLive(live)) return null
|
|
97
|
-
return livePartsOf(asset, { ...live, values: { ...
|
|
100
|
+
return livePartsOf(revision.asset, { ...live, values: { ...revision.inputs, ...live.values } })
|
|
98
101
|
}
|
|
99
102
|
|