@things-factory/figure-ui 10.1.203 → 10.1.205
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 +91 -9
- package/client/modeller/editor-styles.ts +7 -22
- package/client/modeller/editor.ts +176 -145
- package/client/modeller/fasten-form.ts +91 -9
- package/client/modeller/figure-fasten-dialog.ts +42 -9
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +12 -1
- package/client/modeller/links.ts +48 -6
- package/client/modeller/section-drawing.ts +7 -7
- package/client/modeller/size-drag.ts +112 -0
- package/client/modeller/slot-layout-host.ts +3 -3
- package/client/modeller/stage-model.ts +6 -9
- package/client/pages/figure-modeller-page.ts +1 -4
- package/dist-client/modeller/canvas.d.ts +24 -3
- package/dist-client/modeller/canvas.js +96 -11
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +7 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +28 -32
- package/dist-client/modeller/editor.js +176 -153
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +36 -3
- package/dist-client/modeller/fasten-form.js +86 -8
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +17 -3
- package/dist-client/modeller/figure-fasten-dialog.js +47 -10
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +33 -1
- package/dist-client/modeller/figure-fasten-drawing.js +235 -11
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +3 -3
- package/dist-client/modeller/figure-section-editor.js +4 -4
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +12 -1
- package/dist-client/modeller/gizmo.js +8 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/links.d.ts +14 -2
- package/dist-client/modeller/links.js +42 -4
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +3 -4
- package/dist-client/modeller/section-drawing.js +10 -9
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/size-drag.d.ts +38 -0
- package/dist-client/modeller/size-drag.js +85 -0
- package/dist-client/modeller/size-drag.js.map +1 -0
- package/dist-client/modeller/slot-layout-host.d.ts +1 -3
- package/dist-client/modeller/slot-layout-host.js +3 -3
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +5 -7
- package/dist-client/modeller/stage-model.js +6 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +0 -2
- package/dist-client/pages/figure-modeller-page.js +1 -6
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +7 -11
- package/test/browser/acceptance-page.js +10 -0
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +94 -56
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +32 -10
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +2 -2
- package/test/fasten-form.test.ts +82 -2
- package/test/gizmo.test.ts +13 -15
- package/test/links.test.ts +8 -3
- package/test/resize-with-placement.test.ts +14 -13
- package/test/revision.test.ts +3 -2
- package/test/section-editor.test.ts +9 -10
- package/test/size-drag.test.ts +114 -0
- package/test/slot-layout-host.test.ts +13 -8
- package/translations/en.json +25 -22
- package/translations/es.json +25 -22
- package/translations/ja.json +25 -22
- package/translations/ko.json +25 -22
- package/translations/ms.json +25 -22
- package/translations/zh.json +25 -22
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { compileFigureDocument, figurePlacementOf, figureShapeReachOf, figureSizeEditOf, figureSizeEditorOf } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureAsset, FigureInputChange, FigureSizeKeep } from '@hatiolab/figure-model'
|
|
3
|
+
|
|
4
|
+
import { ownerOf } from './fields.js'
|
|
5
|
+
import type { FigureRenderPart } from './renderer.js'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* A size drag drawn from figure-model, frame by frame (ADR-0140 4.4). The gizmo says only which handle and how far; the
|
|
9
|
+
* one size function (`figureSizeEditorOf`) turns the sizes into the changes a commit writes, and the instance evaluator
|
|
10
|
+
* the figure is compiled into once, when the drag begins, steps to them (`advance`). What is drawn while dragging is
|
|
11
|
+
* what the release writes: the same function, the same sizes, the same keep. Nothing drawn goes back into what the
|
|
12
|
+
* gizmo computes (2026-09-24: the gizmo fought the modeller when evaluations were written back onto it).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface SizeDragFrame {
|
|
16
|
+
/** What a release now would write: the sizes and, for a free part, its own position. */
|
|
17
|
+
changes: FigureInputChange[]
|
|
18
|
+
/**
|
|
19
|
+
* Every body moved or reshaped since the drag began, by id: its pose and shape at this frame as figure-model evaluates
|
|
20
|
+
* them. The evaluator tells only what changed since the frame before; a body that moved and then stood still is kept.
|
|
21
|
+
*/
|
|
22
|
+
moved: Map<string, Pick<FigureRenderPart, 'pose' | 'shape'>>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SizeDrag {
|
|
26
|
+
part: string
|
|
27
|
+
keep: FigureSizeKeep
|
|
28
|
+
frame(sizes: readonly FigureInputChange[]): SizeDragFrame
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Pose = FigureRenderPart['pose']
|
|
32
|
+
|
|
33
|
+
/** The drag of `part`'s size from `inputs` (the figure's inputs as it is drawn), keeping what `keep` says. */
|
|
34
|
+
export function startSizeDrag(asset: FigureAsset, part: string, inputs: Record<string, number>, keep: FigureSizeKeep): SizeDrag {
|
|
35
|
+
const edit = figureSizeEditorOf(asset, part)
|
|
36
|
+
const evaluator = compileFigureDocument(asset.document).evaluator({ changes: true })
|
|
37
|
+
const base = { ...asset.designInputs, ...asset.stateDefaults, ...inputs }
|
|
38
|
+
/* The bodies as the evaluator numbers its slots: the order of this first evaluation. */
|
|
39
|
+
let ids = evaluator.evaluate(base).geometry.map(g => (g as { id: string }).id)
|
|
40
|
+
const moved = new Map<string, Pick<FigureRenderPart, 'pose' | 'shape'>>()
|
|
41
|
+
return {
|
|
42
|
+
part,
|
|
43
|
+
keep,
|
|
44
|
+
frame(sizes) {
|
|
45
|
+
const { changes } = edit(sizes, keep)
|
|
46
|
+
const next = { ...base }
|
|
47
|
+
for (const c of changes) next[c.input] = c.value as number
|
|
48
|
+
const step = evaluator.advance(next)
|
|
49
|
+
if (step.kind === 'changes' && step.structure) ids = step.structure.ids
|
|
50
|
+
for (let k = 0; k < step.bodies.length; k++) {
|
|
51
|
+
const id = ids[step.bodies[k]!]
|
|
52
|
+
const p = step.poses.subarray(k * 12, k * 12 + 12)
|
|
53
|
+
if (id === undefined || Number.isNaN(p[0]!)) continue
|
|
54
|
+
const pose: Pose = { r: [[p[0]!, p[1]!, p[2]!], [p[3]!, p[4]!, p[5]!], [p[6]!, p[7]!, p[8]!]], t: [p[9]!, p[10]!, p[11]!] }
|
|
55
|
+
/* The evaluator's shapes are frozen; the scene's builder writes into what it is given, so the canvas gets its own copy. */
|
|
56
|
+
const frozen = step.kind === 'changes' ? step.shapes[k] : undefined
|
|
57
|
+
const shape = frozen ? structuredClone(frozen) : undefined
|
|
58
|
+
const had = moved.get(id)
|
|
59
|
+
moved.set(id, { pose, ...(shape ? { shape: shape as unknown as FigureRenderPart['shape'] } : had?.shape ? { shape: had.shape } : {}) } as Pick<FigureRenderPart, 'pose' | 'shape'>)
|
|
60
|
+
}
|
|
61
|
+
return { changes, moved: new Map(moved) }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What a size typed for this part keeps, in figure-model's words: its middle (`centre`), its fastening (`relation`), or
|
|
68
|
+
* no point -- its position is worked out, and only the size changes (`none`). The words beside the size fields (ADR-0140
|
|
69
|
+
* 4.1, chief architect 10-20); nothing when the part cannot be asked.
|
|
70
|
+
*/
|
|
71
|
+
export function keptBySizeOf(asset: FigureAsset, part: string): 'centre' | 'relation' | 'none' | undefined {
|
|
72
|
+
try {
|
|
73
|
+
const kept = figureSizeEditOf(asset, part, [], { keep: 'centre' }).kept
|
|
74
|
+
return kept === 'opposite' ? undefined : kept
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The words for what a size keeps, by `keptBySizeOf`'s answer. */
|
|
81
|
+
export const SIZE_KEEPS_WORDS = { centre: 'figure.text.size-keeps-centre', relation: 'figure.text.size-keeps-fastening', none: 'figure.text.size-keeps-none' } as const
|
|
82
|
+
|
|
83
|
+
/** The parts drawn this frame: each one the frame moved, with its new pose and shape; every other one as it was. */
|
|
84
|
+
export function partsAtFrame(parts: readonly FigureRenderPart[], moved: SizeDragFrame['moved']): FigureRenderPart[] {
|
|
85
|
+
if (!moved.size) return parts as FigureRenderPart[]
|
|
86
|
+
return parts.map(part => {
|
|
87
|
+
const now = moved.get(part.id)
|
|
88
|
+
return now ? { ...part, pose: now.pose, ...(now.shape ? { shape: now.shape } : {}) } : part
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The side of its mounting plane a figure uses (ADR-0097 4.2): up from a floor, down from a ceiling, out of a wall. */
|
|
93
|
+
const USED: Record<string, [number, number, number] | undefined> = { floor: [0, 1, 0], ceiling: [0, -1, 0], wall: [0, 0, 1], center: undefined }
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* How far a free part goes past its figure's mounting plane, in millimetres, as drawn: its real shape's extreme against
|
|
97
|
+
* the side the placement uses (figure-model's reach, the rule a fastening stands a part by). 0 when it does not, for a
|
|
98
|
+
* fastened part -- its fastening places it -- and for a figure placed by its centre (ADR-0140 5).
|
|
99
|
+
*/
|
|
100
|
+
export function throughPlaneOf(asset: FigureAsset, parts: readonly FigureRenderPart[], part: string): number {
|
|
101
|
+
const n = USED[figurePlacementOf(asset)]
|
|
102
|
+
if (!n || (asset.joints ?? []).some(j => j.body1.part === part)) return 0
|
|
103
|
+
let lowest = Infinity
|
|
104
|
+
for (const body of parts) {
|
|
105
|
+
if (ownerOf(body.id) !== part) continue
|
|
106
|
+
const { r, t } = body.pose
|
|
107
|
+
/* The side's direction in the body's own frame: rᵀ n. */
|
|
108
|
+
const d = [0, 1, 2].map(j => r[0]![j]! * n[0] + r[1]![j]! * n[1] + r[2]![j]! * n[2]) as [number, number, number]
|
|
109
|
+
lowest = Math.min(lowest, t[0]! * n[0] + t[1]! * n[1] + t[2]! * n[2] + figureShapeReachOf(body.shape as never, d).min)
|
|
110
|
+
}
|
|
111
|
+
return Number.isFinite(lowest) && lowest < 0 ? Math.round(-lowest * 10) / 10 : 0
|
|
112
|
+
}
|
|
@@ -66,14 +66,14 @@ export function anchorFacesOf(anchors: readonly SlotAnchor[], geometry: readonly
|
|
|
66
66
|
* a one-anchor preset on the first, a twin fork on the first two. A single slot stands on a top and hangs from a bottom.
|
|
67
67
|
* The editor's own controls then make it what the equipment is.
|
|
68
68
|
*/
|
|
69
|
-
export function slotPresetsOf(anchors: readonly SlotAnchor[]
|
|
69
|
+
export function slotPresetsOf(anchors: readonly SlotAnchor[]) {
|
|
70
70
|
const first = anchors[0]
|
|
71
71
|
const on = { ...(first ? { anchor: first.id } : {}), ...(anchors.length >= 2 ? { forks: [anchors[0]!.id, anchors[1]!.id] as [string, string] } : {}) }
|
|
72
72
|
const mount = first?.face === 'bottom' ? ('HANG' as const) : ('ON' as const)
|
|
73
73
|
return SLOT_LAYOUT_PRESET_KEYS.map(key => ({
|
|
74
74
|
key,
|
|
75
|
-
|
|
76
|
-
params: SLOT_LAYOUT_PRESET_PARAMS[key].map(p => ({ key: p.key,
|
|
75
|
+
/* The editor names a preset and its values from their keys (@operato/slot-layout-editor's own words). */
|
|
76
|
+
params: SLOT_LAYOUT_PRESET_PARAMS[key].map(p => ({ key: p.key, integer: p.integer })),
|
|
77
77
|
build: (size: SlotPresetSize, params: Record<string, number>) => slotLayoutPreset(key, size, params, { ...on, mount })
|
|
78
78
|
}))
|
|
79
79
|
}
|
|
@@ -64,16 +64,13 @@ function colours(part: FigureRenderPart, palette: Record<string, string>) {
|
|
|
64
64
|
return { color: palette[m.token] ?? '#9a9a9a', preset: m.preset, transparent: !!m.transparent, ...(glow ? { glow } : {}) }
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/** Per axis of the body's own frame, the face a size drag keeps: 1 the low face, -1 the high face, 0 the centre. */
|
|
68
|
-
export type ScaleAnchor = Record<'x' | 'y' | 'z', -1 | 0 | 1>
|
|
69
|
-
|
|
70
67
|
/**
|
|
71
|
-
* The component state that draws a body where figure-model put it.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
68
|
+
* The component state that draws a body where figure-model put it. A part the author edits is `owned`: while its size
|
|
69
|
+
* is dragged the scene does not move it and tells the handle held (`scaleGrab`), and the modeller draws where
|
|
70
|
+
* figure-model's size function puts it, every frame (ADR-0140 4.4). Body x · y · z are the component's width · depth ·
|
|
71
|
+
* height, as the scene's axes are.
|
|
75
72
|
*/
|
|
76
|
-
export function componentOf(part: FigureRenderPart, palette: Record<string, string>, board: Board,
|
|
73
|
+
export function componentOf(part: FigureRenderPart, palette: Record<string, string>, board: Board, owned = false): Record<string, unknown> | null {
|
|
77
74
|
const frame = frameOf(part)
|
|
78
75
|
if (!frame) return null
|
|
79
76
|
const { centre, extent, rotation, origin } = frame
|
|
@@ -97,7 +94,7 @@ export function componentOf(part: FigureRenderPart, palette: Record<string, stri
|
|
|
97
94
|
rotationX: round(euler.x),
|
|
98
95
|
rotation: round(-euler.y),
|
|
99
96
|
rotationY: round(euler.z),
|
|
100
|
-
...(
|
|
97
|
+
...(owned ? { scalePlacement: 'owner' } : {})
|
|
101
98
|
}
|
|
102
99
|
}
|
|
103
100
|
|
|
@@ -147,8 +147,6 @@ export class FigureModellerPage extends PageView {
|
|
|
147
147
|
@state() private commandError = ''
|
|
148
148
|
/** What the verdict says blocks release now, handed to the editor's bar. */
|
|
149
149
|
@state() private releaseBlockers: FigureGateFinding[] = []
|
|
150
|
-
/** The verdict's count, handed to the editor's verdict tab. */
|
|
151
|
-
@state() private instances?: number
|
|
152
150
|
@state() private budget?: FigureBudgetLine
|
|
153
151
|
private wasDirty = false
|
|
154
152
|
private titled?: string | null
|
|
@@ -532,7 +530,7 @@ export class FigureModellerPage extends PageView {
|
|
|
532
530
|
if (this.error) return html`<p role="alert" class="error">${this.error}</p>`
|
|
533
531
|
const view = this.session.read()
|
|
534
532
|
/* A new figure's third way to start: the app's assistant dock, with a request put in its input for the author to finish and send (B, U04). */
|
|
535
|
-
return html`<figure-editor .session=${this.session} .releaseState=${this.row} .releaseBlockers=${this.releaseBlockers} .templates=${this.templates} .guideOf=${this.guideOf} .thumbnailOf=${this.thumbnailOf} .firstSave=${this.askFirstSave} .revision=${this.revision} .
|
|
533
|
+
return html`<figure-editor .session=${this.session} .releaseState=${this.row} .releaseBlockers=${this.releaseBlockers} .templates=${this.templates} .guideOf=${this.guideOf} .thumbnailOf=${this.thumbnailOf} .firstSave=${this.askFirstSave} .revision=${this.revision} .budget=${this.budget} aiDraft @ask-ai-draft=${() => askAssistant({ draft: i18next.t('figure.text.ai-draft-request') })} @changed=${this.changed}>
|
|
536
534
|
${this.commandError ? html`<p slot="notice" fit-error role="alert">${this.commandError}</p>` : ''}
|
|
537
535
|
<figure-properties
|
|
538
536
|
slot="figure"
|
|
@@ -562,7 +560,6 @@ export class FigureModellerPage extends PageView {
|
|
|
562
560
|
.figureId=${view.id}
|
|
563
561
|
.figureType=${this.row.type}
|
|
564
562
|
@capacity-changed=${(event: CustomEvent) => {
|
|
565
|
-
this.instances = event.detail.instances
|
|
566
563
|
this.budget = event.detail.budget
|
|
567
564
|
}}
|
|
568
565
|
@fit-base-to-parts=${this.fitToParts}
|
|
@@ -2,16 +2,19 @@ import { LitElement, type PropertyValues } from 'lit';
|
|
|
2
2
|
import type { GizmoReach } from './gizmo.js';
|
|
3
3
|
import type { FigureRenderPart } from './renderer.js';
|
|
4
4
|
import { type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js';
|
|
5
|
-
import { type ScaleAnchor } from './stage-model.js';
|
|
6
5
|
export declare class FigureCanvas extends LitElement {
|
|
7
6
|
static styles: import("lit").CSSResult;
|
|
8
7
|
parts: FigureRenderPart[];
|
|
9
8
|
palette: Record<string, string>;
|
|
10
|
-
/** The face a size drag keeps, per part (figure-model's `figureScaleAnchorOf`), so the drag shows what is written. */
|
|
11
|
-
anchors: Record<string, ScaleAnchor>;
|
|
12
9
|
/** Which figure is open. Another one stands the board up again. */
|
|
13
10
|
context: string;
|
|
14
11
|
selected: string;
|
|
12
|
+
/**
|
|
13
|
+
* The fastening window is open (ADR-0140 stage 4): a part pressed is aimed at, not chosen -- told out as `part-aimed`
|
|
14
|
+
* with where the press landed (the scene's `pickedAt`: the point and the drawn triangle's outward normal, in the
|
|
15
|
+
* figure's frame), and the part being fastened stays chosen.
|
|
16
|
+
*/
|
|
17
|
+
aiming: boolean;
|
|
15
18
|
/** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */
|
|
16
19
|
reach: GizmoReach;
|
|
17
20
|
view: ViewSettings;
|
|
@@ -61,6 +64,13 @@ export declare class FigureCanvas extends LitElement {
|
|
|
61
64
|
* one that has outgrown it -- standing up again loses the selection and the camera.
|
|
62
65
|
*/
|
|
63
66
|
private standUp;
|
|
67
|
+
/**
|
|
68
|
+
* A part's component state, kept for the part object it was made from while the palette and the board are the same: a
|
|
69
|
+
* size drag redraws the board every frame, and only the parts the frame moved are new objects (ADR-0140 4.4).
|
|
70
|
+
*/
|
|
71
|
+
private made;
|
|
72
|
+
private madeFor?;
|
|
73
|
+
private componentFor;
|
|
64
74
|
private components;
|
|
65
75
|
/** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */
|
|
66
76
|
private standing;
|
|
@@ -72,9 +82,20 @@ export declare class FigureCanvas extends LitElement {
|
|
|
72
82
|
private watchScene;
|
|
73
83
|
/** Components the gizmo changed since the last commit, by part name. */
|
|
74
84
|
private pending;
|
|
85
|
+
/** The handle each size drag held, by part name, from the scene's `scaleGrab` -- told with the release. */
|
|
86
|
+
private grabs;
|
|
87
|
+
private sizingFrame?;
|
|
88
|
+
/** Once an animation frame: where a size drag stands, told so the editor draws figure-model's result for it. */
|
|
89
|
+
private sizingSoon;
|
|
75
90
|
private pointerDown;
|
|
76
91
|
private commitTimer?;
|
|
92
|
+
/**
|
|
93
|
+
* The parts as they stood when the pointer went down. A drag is measured against them: while a size drag is drawn, the
|
|
94
|
+
* parts given are its frames (ADR-0140 4.4), and measured against those it grew on itself and its release wrote nothing.
|
|
95
|
+
*/
|
|
96
|
+
private atPress?;
|
|
77
97
|
private onPointerDown;
|
|
98
|
+
private pressedPart;
|
|
78
99
|
private onPointerUp;
|
|
79
100
|
private commitSoon;
|
|
80
101
|
/** Tell each changed part once, in the inputs' terms; then put the board where the inputs say (a refusal springs back). */
|
|
@@ -19,11 +19,15 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
19
19
|
super(...arguments);
|
|
20
20
|
this.parts = [];
|
|
21
21
|
this.palette = {};
|
|
22
|
-
/** The face a size drag keeps, per part (figure-model's `figureScaleAnchorOf`), so the drag shows what is written. */
|
|
23
|
-
this.anchors = {};
|
|
24
22
|
/** Which figure is open. Another one stands the board up again. */
|
|
25
23
|
this.context = '';
|
|
26
24
|
this.selected = '';
|
|
25
|
+
/**
|
|
26
|
+
* The fastening window is open (ADR-0140 stage 4): a part pressed is aimed at, not chosen -- told out as `part-aimed`
|
|
27
|
+
* with where the press landed (the scene's `pickedAt`: the point and the drawn triangle's outward normal, in the
|
|
28
|
+
* figure's frame), and the part being fastened stays chosen.
|
|
29
|
+
*/
|
|
30
|
+
this.aiming = false;
|
|
27
31
|
/** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */
|
|
28
32
|
this.reach = { translate: false, rotate: false, scale: false };
|
|
29
33
|
this.view = DEFAULT_VIEW;
|
|
@@ -45,11 +49,19 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
45
49
|
this.marks = [];
|
|
46
50
|
/** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */
|
|
47
51
|
this.writing = false;
|
|
52
|
+
/**
|
|
53
|
+
* A part's component state, kept for the part object it was made from while the palette and the board are the same: a
|
|
54
|
+
* size drag redraws the board every frame, and only the parts the frame moved are new objects (ADR-0140 4.4).
|
|
55
|
+
*/
|
|
56
|
+
this.made = new WeakMap();
|
|
48
57
|
/** Components the gizmo changed since the last commit, by part name. */
|
|
49
58
|
this.pending = new Map();
|
|
59
|
+
/** The handle each size drag held, by part name, from the scene's `scaleGrab` -- told with the release. */
|
|
60
|
+
this.grabs = new Map();
|
|
50
61
|
this.pointerDown = false;
|
|
51
62
|
this.onPointerDown = () => {
|
|
52
63
|
this.pointerDown = true;
|
|
64
|
+
this.atPress = new Map(this.parts.map(p => [p.id, p]));
|
|
53
65
|
};
|
|
54
66
|
this.onPointerUp = () => {
|
|
55
67
|
if (!this.pointerDown)
|
|
@@ -94,7 +106,7 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
94
106
|
updated(changed) {
|
|
95
107
|
if (!this.hasUpdated)
|
|
96
108
|
return;
|
|
97
|
-
if (['parts', 'palette', 'context', 'fan', '
|
|
109
|
+
if (['parts', 'palette', 'context', 'fan', 'ghost', 'highlight', 'marks'].some(key => changed.has(key)))
|
|
98
110
|
this.standUp();
|
|
99
111
|
if (changed.has('view'))
|
|
100
112
|
this.applyView();
|
|
@@ -201,8 +213,15 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
201
213
|
this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }));
|
|
202
214
|
}
|
|
203
215
|
}
|
|
216
|
+
componentFor(part) {
|
|
217
|
+
if (this.madeFor?.[0] !== this.palette || this.madeFor?.[1] !== this.board)
|
|
218
|
+
(this.made = new WeakMap()), (this.madeFor = [this.palette, this.board]);
|
|
219
|
+
if (!this.made.has(part))
|
|
220
|
+
this.made.set(part, componentOf(part, this.palette, this.board, true));
|
|
221
|
+
return this.made.get(part);
|
|
222
|
+
}
|
|
204
223
|
components() {
|
|
205
|
-
const parts = this.parts.map(part =>
|
|
224
|
+
const parts = this.parts.map(part => this.componentFor(part)).filter((c) => !!c);
|
|
206
225
|
/* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */
|
|
207
226
|
const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board) : null;
|
|
208
227
|
const accent = this.palette['palette.accent'] ?? FAN_COLOUR;
|
|
@@ -253,7 +272,15 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
253
272
|
const found = standing.get(name);
|
|
254
273
|
if (found) {
|
|
255
274
|
const diff = changedKeys(found.state, state);
|
|
256
|
-
|
|
275
|
+
/*
|
|
276
|
+
A part whose size is being dragged keeps the size the gizmo gives it: what is drawn does not go back into what the
|
|
277
|
+
gizmo measures (ADR-0140 4.4). Written back, a size the frame could not change (one that follows the figure) put
|
|
278
|
+
the part back to its old size, and the release wrote nothing.
|
|
279
|
+
*/
|
|
280
|
+
if (diff && this.pointerDown && this.grabs.has(name))
|
|
281
|
+
for (const key of ['width', 'depth', 'height'])
|
|
282
|
+
delete diff[key];
|
|
283
|
+
if (diff && Object.keys(diff).length)
|
|
257
284
|
found.set(diff);
|
|
258
285
|
continue;
|
|
259
286
|
}
|
|
@@ -281,11 +308,35 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
281
308
|
*/
|
|
282
309
|
if (!this.isConnected)
|
|
283
310
|
return;
|
|
311
|
+
/*
|
|
312
|
+
While aiming, a press is told by the click (below); the part being fastened is chosen again once the scene has
|
|
313
|
+
done with the pick -- put back while the scene was still telling it, the selection read right but the outline
|
|
314
|
+
stayed on the part pressed (:3300, 2026-10-20).
|
|
315
|
+
*/
|
|
316
|
+
if (this.aiming) {
|
|
317
|
+
setTimeout(() => this.showSelected());
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
284
320
|
const name = after?.[0]?.state?.name;
|
|
285
321
|
const id = typeof name === 'string' ? name : '';
|
|
286
322
|
if (ownerOf(id) !== this.selected)
|
|
287
323
|
this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }));
|
|
288
324
|
});
|
|
325
|
+
/*
|
|
326
|
+
A press while aiming: the part pressed -- the one being fastened too, which changes no selection and so is told by
|
|
327
|
+
no `selected` -- with where it landed (`pickedAt`, filled before the click is told). It comes up to the board
|
|
328
|
+
with the component in the last argument's `origin`, as a change does.
|
|
329
|
+
*/
|
|
330
|
+
layer.on?.('click', (...args) => {
|
|
331
|
+
if (!this.aiming || !this.isConnected)
|
|
332
|
+
return;
|
|
333
|
+
const origin = args[args.length - 1]?.origin;
|
|
334
|
+
const id = origin?.state?.name;
|
|
335
|
+
if (typeof id !== 'string' || !id)
|
|
336
|
+
return;
|
|
337
|
+
const at = origin?.realObject?.pickedAt;
|
|
338
|
+
this.dispatchEvent(new CustomEvent('part-aimed', { detail: { id, ...(at ? { point: at.point, normal: at.normal } : {}) }, bubbles: true, composed: true }));
|
|
339
|
+
});
|
|
289
340
|
/*
|
|
290
341
|
A change comes up to the board with the component in the last argument's `origin` (the scene appends
|
|
291
342
|
{ origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing.
|
|
@@ -305,6 +356,12 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
305
356
|
if (typeof name !== 'string' || !this.parts.some(p => p.id === name))
|
|
306
357
|
return;
|
|
307
358
|
this.pending.set(name, origin);
|
|
359
|
+
/* A size drag: the scene tells the handle held; figure-model draws each frame from it (ADR-0140 4.4). */
|
|
360
|
+
const grab = origin.state?.scaleGrab;
|
|
361
|
+
if (this.pointerDown && grab) {
|
|
362
|
+
this.grabs.set(name, { ...grab });
|
|
363
|
+
this.sizingSoon(name);
|
|
364
|
+
}
|
|
308
365
|
if (!this.pointerDown)
|
|
309
366
|
this.commitSoon(80);
|
|
310
367
|
});
|
|
@@ -312,6 +369,24 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
312
369
|
window.addEventListener('pointerup', this.onPointerUp, true);
|
|
313
370
|
window.addEventListener('pointercancel', this.onPointerUp, true);
|
|
314
371
|
}
|
|
372
|
+
/** Once an animation frame: where a size drag stands, told so the editor draws figure-model's result for it. */
|
|
373
|
+
sizingSoon(name) {
|
|
374
|
+
if (this.sizingFrame !== undefined)
|
|
375
|
+
return;
|
|
376
|
+
this.sizingFrame = requestAnimationFrame(() => {
|
|
377
|
+
this.sizingFrame = undefined;
|
|
378
|
+
const part = this.pressedPart(name);
|
|
379
|
+
const component = this.pending.get(name);
|
|
380
|
+
if (!part || !component || !this.board || !this.pointerDown)
|
|
381
|
+
return;
|
|
382
|
+
const result = { ...gizmoResultOf(part, component.state, this.board), grab: this.grabs.get(name) };
|
|
383
|
+
if (result.scale)
|
|
384
|
+
this.dispatchEvent(new CustomEvent('part-sizing', { detail: { id: part.id, result }, bubbles: true, composed: true }));
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
pressedPart(name) {
|
|
388
|
+
return this.atPress?.get(name) ?? this.parts.find(p => p.id === name);
|
|
389
|
+
}
|
|
315
390
|
commitSoon(delay) {
|
|
316
391
|
clearTimeout(this.commitTimer);
|
|
317
392
|
this.commitTimer = setTimeout(() => this.commit(), delay);
|
|
@@ -327,15 +402,21 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
327
402
|
return;
|
|
328
403
|
const changed = [...this.pending];
|
|
329
404
|
this.pending.clear();
|
|
405
|
+
if (this.sizingFrame !== undefined)
|
|
406
|
+
cancelAnimationFrame(this.sizingFrame);
|
|
407
|
+
this.sizingFrame = undefined;
|
|
330
408
|
for (const [name, component] of changed) {
|
|
331
|
-
const part = this.
|
|
409
|
+
const part = this.pressedPart(name);
|
|
332
410
|
if (!part)
|
|
333
411
|
continue;
|
|
334
|
-
const
|
|
412
|
+
const grab = this.grabs.get(name);
|
|
413
|
+
const result = { ...gizmoResultOf(part, component.state, this.board), ...(grab ? { grab } : {}) };
|
|
335
414
|
if (!result.position && !result.rotation && !result.scale)
|
|
336
415
|
continue;
|
|
337
416
|
this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }));
|
|
338
417
|
}
|
|
418
|
+
this.grabs.clear();
|
|
419
|
+
this.atPress = undefined;
|
|
339
420
|
setTimeout(() => this.sync());
|
|
340
421
|
}
|
|
341
422
|
showSelected() {
|
|
@@ -362,6 +443,10 @@ let FigureCanvas = class FigureCanvas extends LitElement {
|
|
|
362
443
|
teardown() {
|
|
363
444
|
clearTimeout(this.commitTimer);
|
|
364
445
|
this.pending.clear();
|
|
446
|
+
this.grabs.clear();
|
|
447
|
+
if (this.sizingFrame !== undefined)
|
|
448
|
+
cancelAnimationFrame(this.sizingFrame);
|
|
449
|
+
this.sizingFrame = undefined;
|
|
365
450
|
this.stage?.removeEventListener('pointerdown', this.onPointerDown, true);
|
|
366
451
|
window.removeEventListener('pointerup', this.onPointerUp, true);
|
|
367
452
|
window.removeEventListener('pointercancel', this.onPointerUp, true);
|
|
@@ -379,10 +464,6 @@ __decorate([
|
|
|
379
464
|
property({ attribute: false }),
|
|
380
465
|
__metadata("design:type", Object)
|
|
381
466
|
], FigureCanvas.prototype, "palette", void 0);
|
|
382
|
-
__decorate([
|
|
383
|
-
property({ attribute: false }),
|
|
384
|
-
__metadata("design:type", Object)
|
|
385
|
-
], FigureCanvas.prototype, "anchors", void 0);
|
|
386
467
|
__decorate([
|
|
387
468
|
property(),
|
|
388
469
|
__metadata("design:type", Object)
|
|
@@ -391,6 +472,10 @@ __decorate([
|
|
|
391
472
|
property(),
|
|
392
473
|
__metadata("design:type", Object)
|
|
393
474
|
], FigureCanvas.prototype, "selected", void 0);
|
|
475
|
+
__decorate([
|
|
476
|
+
property({ type: Boolean }),
|
|
477
|
+
__metadata("design:type", Object)
|
|
478
|
+
], FigureCanvas.prototype, "aiming", void 0);
|
|
394
479
|
__decorate([
|
|
395
480
|
property({ attribute: false }),
|
|
396
481
|
__metadata("design:type", Object)
|