@things-factory/figure-ui 10.1.128 → 10.1.141
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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { compileFigureAsset } from '@hatiolab/figure-model'
|
|
2
|
-
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
3
2
|
|
|
4
3
|
import { PREVIEW, TARGET_FACE } from './command-preview.js'
|
|
5
4
|
import type { FigureRenderPart } from './renderer.js'
|
|
6
5
|
import { boxShape } from './drawn-shapes.js'
|
|
6
|
+
import type { FigureRevision } from './revision.js'
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
9
|
* Where a part's motion takes it (ADR-0097 §5.5, designer 2026-09-27): the bodies its control moves, drawn translucent at
|
|
@@ -29,20 +29,19 @@ export interface MotionEnds {
|
|
|
29
29
|
const samePose = (a: Pose, b: Pose) => [0, 1, 2].every(i => Math.abs(a.t[i]! - b.t[i]!) < 1e-6 && [0, 1, 2].every(k => Math.abs(a.r[i]![k]! - b.r[i]![k]!) < 1e-9))
|
|
30
30
|
|
|
31
31
|
/** The ends of the selected part's own motion, or nothing: a part that does not move, follows another, or turns without end. */
|
|
32
|
-
export function motionEndsOf(
|
|
32
|
+
export function motionEndsOf(revision: FigureRevision, part: string): MotionEnds | null {
|
|
33
|
+
const asset = revision.asset
|
|
33
34
|
const joint = asset.joints?.find(j => j.body1.part === part)
|
|
34
35
|
/* A turn without end is a revolute joint whose control has no range: the range check below leaves it out. */
|
|
35
36
|
if (!joint || joint.type === 'fixed' || joint.mimic) return null
|
|
36
37
|
const control = joint.state?.id ?? joint.id
|
|
37
38
|
const input = asset.document.model.inputs.find(i => i.id === control && i.role === 'state')
|
|
38
39
|
if (!input || typeof input.min !== 'number' || typeof input.max !== 'number' || input.min === input.max) return null
|
|
39
|
-
/* The values
|
|
40
|
-
const
|
|
41
|
-
const shown = { ...asset, designInputs: { ...asset.designInputs, ...pick(asset.designInputs) } }
|
|
42
|
-
const state = pick(asset.stateDefaults)
|
|
40
|
+
/* The version's design values are its asset's; its pose is every control as set now, this one at each end. */
|
|
41
|
+
const state = Object.fromEntries(Object.entries(revision.inputs).filter(([id]) => id in asset.stateDefaults))
|
|
43
42
|
const at = (value: number) => {
|
|
44
43
|
try {
|
|
45
|
-
return compileFigureAsset(
|
|
44
|
+
return compileFigureAsset(asset).evaluate({ ...state, [control]: value }).geometry as unknown as Body[]
|
|
46
45
|
} catch {
|
|
47
46
|
return null
|
|
48
47
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { proposeFigureOccupancy, type FigureAsset, type FigureAuthoringAction, type FigureRoomRefit } from '@hatiolab/figure-model'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 「부품에 맞추기」의 짝 — 공간을 지금 부품에서 다시 선언하는 한 걸음.
|
|
@@ -13,11 +13,10 @@ import { parseFigureAsset, proposeFigureOccupancy, type FigureAuthoringAction, t
|
|
|
13
13
|
* 모델러 기본이 바닥이다.
|
|
14
14
|
*/
|
|
15
15
|
export function occupancyFromParts(
|
|
16
|
-
|
|
16
|
+
asset: FigureAsset,
|
|
17
17
|
/** A different face to place it by — the properties tab's choice. Absent keeps the declared one. */
|
|
18
18
|
placement?: 'floor' | 'ceiling' | 'center' | 'wall'
|
|
19
19
|
): Extract<FigureAuthoringAction, { kind: 'declare-occupancy' }> {
|
|
20
|
-
const asset = parseFigureAsset(source)
|
|
21
20
|
return {
|
|
22
21
|
kind: 'declare-occupancy',
|
|
23
22
|
proposal: proposeFigureOccupancy(asset, { over: 'range' }),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { thumbnailHashOf } from './thumbnail-hash.js'
|
|
2
|
+
import { revisionFromView, type FigureRevision } from './revision.js'
|
|
2
3
|
import {
|
|
3
4
|
createFigureAsset,
|
|
4
5
|
createFigureEditorWorkspace,
|
|
@@ -74,6 +75,18 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
74
75
|
function read() {
|
|
75
76
|
return { ...workspace.read(), id, loading, saving, error }
|
|
76
77
|
}
|
|
78
|
+
/*
|
|
79
|
+
The version on screen, one object for as long as it is the version: the workspace hands out one asset per version
|
|
80
|
+
(and another once a save is acknowledged, or the figure moves on), so that object says whether the last one holds.
|
|
81
|
+
*/
|
|
82
|
+
let shown: FigureRevision | undefined
|
|
83
|
+
function revisionNow(): FigureRevision | undefined {
|
|
84
|
+
const view = workspace.read()
|
|
85
|
+
if (!view.context || view.revision === null) return (shown = undefined)
|
|
86
|
+
const context = view.context
|
|
87
|
+
if (shown?.asset !== view.asset) shown = revisionFromView(view, () => workspace.export({ context }).text)
|
|
88
|
+
return shown
|
|
89
|
+
}
|
|
77
90
|
/* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */
|
|
78
91
|
const unconfirmed = new Map<string, { thumbnail?: string }>()
|
|
79
92
|
/*
|
|
@@ -116,6 +129,8 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
116
129
|
}
|
|
117
130
|
return {
|
|
118
131
|
read,
|
|
132
|
+
/** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */
|
|
133
|
+
revisionNow,
|
|
119
134
|
/**
|
|
120
135
|
* The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`
|
|
121
136
|
* -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { compileFigureGraph } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureAsset, FigureWorkspaceView } from '@hatiolab/figure-model'
|
|
3
|
+
|
|
4
|
+
type OpenView = Extract<FigureWorkspaceView, { context: string }>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The figure on screen at one version: what every panel and helper of the modeller reads, made once per version by the
|
|
8
|
+
* product session. The asset is the kernel's (`view.asset`, ADR-0119 decision 6): frozen, never changed, and the kernel's
|
|
9
|
+
* questions asked of it under `inputs` are answered without evaluating it again. `geometry` is the figure as drawn --
|
|
10
|
+
* every input as the session holds it now, the pose set with the controls included.
|
|
11
|
+
*
|
|
12
|
+
* A new version is a new object. What is worked out from a version is kept with it (`derivedOf`) and goes with it, so
|
|
13
|
+
* there is one rule for when an answer is stale: it was worked out from another object.
|
|
14
|
+
*/
|
|
15
|
+
export interface FigureRevision {
|
|
16
|
+
readonly context: string
|
|
17
|
+
readonly revision: number
|
|
18
|
+
readonly asset: FigureAsset
|
|
19
|
+
/** Every input as the session holds it now, design and state. */
|
|
20
|
+
readonly inputs: Readonly<Record<string, number>>
|
|
21
|
+
readonly geometry: OpenView['geometry']
|
|
22
|
+
readonly palette: OpenView['palette']
|
|
23
|
+
readonly fields: OpenView['fields']
|
|
24
|
+
/** The source text of this version, as a save sends it. */
|
|
25
|
+
readonly text: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The version a workspace view shows. `text` is the workspace's export of it, asked when first read. */
|
|
29
|
+
export function revisionFromView(view: OpenView, text: () => string): FigureRevision {
|
|
30
|
+
let written: string | undefined
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
context: view.context,
|
|
33
|
+
revision: view.revision,
|
|
34
|
+
asset: view.asset,
|
|
35
|
+
inputs: Object.freeze(Object.fromEntries(view.fields.map(field => [field.id, field.value as number]))),
|
|
36
|
+
geometry: view.geometry,
|
|
37
|
+
palette: view.palette,
|
|
38
|
+
fields: view.fields,
|
|
39
|
+
get text() {
|
|
40
|
+
return (written ??= text())
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const derived = new WeakMap<FigureRevision, Map<string, unknown>>()
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What is worked out from a version, kept with it: asked again of the same version -- a render asks a dozen times -- it
|
|
49
|
+
* is answered from the first time. `key` names the question (the part, the language) within the version.
|
|
50
|
+
*/
|
|
51
|
+
export function derivedOf<T>(revision: FigureRevision, key: string, make: () => T): T {
|
|
52
|
+
let mine = derived.get(revision)
|
|
53
|
+
if (!mine) derived.set(revision, (mine = new Map()))
|
|
54
|
+
if (!mine.has(key)) mine.set(key, make())
|
|
55
|
+
return mine.get(key) as T
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What was worked out from this version under `key`, or nothing yet: for an answer worked out later, once the screen has rested. */
|
|
59
|
+
export function derivedHeld<T>(revision: FigureRevision, key: string): T | undefined {
|
|
60
|
+
return derived.get(revision)?.get(key) as T | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Every value of the figure's graph at this version, as drawn: one evaluation of the graph for every measure read off it. */
|
|
64
|
+
export function graphValuesOf(revision: FigureRevision): Readonly<Record<string, unknown>> {
|
|
65
|
+
return derivedOf(revision, 'graph-values', () => compileFigureGraph(revision.asset.document.model as never).evaluate({ ...revision.inputs }).values as Record<string, unknown>)
|
|
66
|
+
}
|
|
@@ -121,19 +121,6 @@ export class FigureModellerPage extends PageView {
|
|
|
121
121
|
/** The verdict's count, handed to the editor's verdict tab. */
|
|
122
122
|
@state() private instances?: number
|
|
123
123
|
@state() private budget?: FigureBudgetLine
|
|
124
|
-
/**
|
|
125
|
-
* The source the verdict judges. The session refuses to export while it loads or saves, so the last one read is
|
|
126
|
-
* kept rather than blanked — the verdict would otherwise flicker to 「waiting」 on every save.
|
|
127
|
-
*/
|
|
128
|
-
private draft?: string
|
|
129
|
-
private draftSource(): string | undefined {
|
|
130
|
-
try {
|
|
131
|
-
this.draft = this.session.source()
|
|
132
|
-
} catch {
|
|
133
|
-
/* loading or saving — keep the last source */
|
|
134
|
-
}
|
|
135
|
-
return this.draft
|
|
136
|
-
}
|
|
137
124
|
private wasDirty = false
|
|
138
125
|
private titled?: string | null
|
|
139
126
|
/** The figure the AI dock was last told about - another id is another conversation. */
|
|
@@ -278,7 +265,9 @@ export class FigureModellerPage extends PageView {
|
|
|
278
265
|
}
|
|
279
266
|
/** 「부품에 맞추기」 — 공간을 지금 부품에서 다시 선언한다. 저장은 저작자가 따로 한다. */
|
|
280
267
|
private fitToParts = () => {
|
|
281
|
-
|
|
268
|
+
const shown = this.session.revisionNow()
|
|
269
|
+
/* Nothing open, nothing to fit: the button stands only beside an open figure. */
|
|
270
|
+
if (shown) this.runCommand(() => occupancyFromParts(shown.asset))
|
|
282
271
|
}
|
|
283
272
|
/** A command from the properties tab — rename, declare the room, the mounting face. */
|
|
284
273
|
private onCommand = (event: Event) => {
|
|
@@ -425,7 +414,6 @@ export class FigureModellerPage extends PageView {
|
|
|
425
414
|
const discard = this.discardApproved
|
|
426
415
|
this.discardApproved = false
|
|
427
416
|
if (id) {
|
|
428
|
-
this.draft = undefined
|
|
429
417
|
/* The row is read after the figure opens; until then the last figure's type must not stand in the field (C2). */
|
|
430
418
|
this.row = { version: 0 }
|
|
431
419
|
this.typeError = ''
|
|
@@ -435,7 +423,6 @@ export class FigureModellerPage extends PageView {
|
|
|
435
423
|
this.readRow(id)
|
|
436
424
|
} else {
|
|
437
425
|
this.session.startNew({ discard })
|
|
438
|
-
this.draft = undefined
|
|
439
426
|
this.row = { version: 0 }
|
|
440
427
|
this.typeError = ''
|
|
441
428
|
this.takeNewFigureProposal()
|
|
@@ -499,7 +486,7 @@ export class FigureModellerPage extends PageView {
|
|
|
499
486
|
${this.commandError ? html`<p slot="notice" fit-error role="alert">${this.commandError}</p>` : ''}
|
|
500
487
|
<figure-properties
|
|
501
488
|
slot="figure"
|
|
502
|
-
.
|
|
489
|
+
.revision=${this.session.revisionNow()}
|
|
503
490
|
.figureId=${view.id}
|
|
504
491
|
.figureCategory=${this.row.category}
|
|
505
492
|
.figureTags=${this.row.tags}
|
|
@@ -520,7 +507,7 @@ export class FigureModellerPage extends PageView {
|
|
|
520
507
|
</figure-properties>
|
|
521
508
|
<figure-verdict
|
|
522
509
|
slot="verdict"
|
|
523
|
-
.
|
|
510
|
+
.revision=${this.session.revisionNow()}
|
|
524
511
|
.figureId=${view.id}
|
|
525
512
|
.figureType=${this.row.type}
|
|
526
513
|
@capacity-changed=${(event: CustomEvent) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FigureInputChange, FigureWorldBox } from '@hatiolab/figure-model';
|
|
2
|
+
import { type FigureRevision } from './revision.js';
|
|
2
3
|
import type { GizmoReach } from './gizmo.js';
|
|
3
4
|
/**
|
|
4
5
|
* 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,
|
|
@@ -12,18 +13,22 @@ import type { GizmoReach } from './gizmo.js';
|
|
|
12
13
|
* where the gizmo could (`GizmoReach`).
|
|
13
14
|
*/
|
|
14
15
|
export type AlignTool = 'centre-x' | 'centre-z' | 'snap';
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a
|
|
18
|
+
* command sees it (a fastening keeps a part where it is drawn).
|
|
19
|
+
*/
|
|
20
|
+
export declare function boxesOf(revision: FigureRevision): FigureWorldBox[];
|
|
21
|
+
export declare function boxOf(revision: FigureRevision, part: string): FigureWorldBox | undefined;
|
|
17
22
|
/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */
|
|
18
|
-
export declare function gridStepOf(
|
|
19
|
-
export declare function alignChanges(
|
|
23
|
+
export declare function gridStepOf(revision: FigureRevision): number;
|
|
24
|
+
export declare function alignChanges(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[];
|
|
20
25
|
/**
|
|
21
26
|
* Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by
|
|
22
27
|
* `duplicate-part` first; this is the position the copy is then given.
|
|
23
28
|
*/
|
|
24
|
-
export declare function mirrorChanges(
|
|
29
|
+
export declare function mirrorChanges(revision: FigureRevision, part: string, copy: string, axis: 'x' | 'z'): FigureInputChange[];
|
|
25
30
|
/** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */
|
|
26
|
-
export declare function canMirror(
|
|
31
|
+
export declare function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean;
|
|
27
32
|
/**
|
|
28
33
|
* A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.
|
|
29
34
|
* Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model';
|
|
2
|
-
import {
|
|
2
|
+
import { derivedOf } from './revision.js';
|
|
3
3
|
const AXES = ['x', 'y', 'z'];
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a
|
|
6
|
+
* command sees it (a fastening keeps a part where it is drawn).
|
|
7
|
+
*/
|
|
8
|
+
export function boxesOf(revision) {
|
|
9
|
+
return derivedOf(revision, 'boxes', () => figureOccupiedBoxesOf(revision.geometry, figureWritersOf(revision.asset)));
|
|
10
|
+
}
|
|
11
|
+
export function boxOf(revision, part) {
|
|
12
|
+
return boxesOf(revision).find(box => box.part === part);
|
|
8
13
|
}
|
|
9
14
|
/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */
|
|
10
|
-
export function gridStepOf(
|
|
11
|
-
const
|
|
12
|
-
const boxes = figureOccupiedBoxesOf(geometry, figureWritersOf(asset));
|
|
15
|
+
export function gridStepOf(revision) {
|
|
16
|
+
const boxes = boxesOf(revision);
|
|
13
17
|
if (!boxes.length)
|
|
14
18
|
return 1;
|
|
15
19
|
const span = Math.max(...AXES.map(axis => {
|
|
@@ -19,13 +23,13 @@ export function gridStepOf(asset) {
|
|
|
19
23
|
}));
|
|
20
24
|
return niceStep(span);
|
|
21
25
|
}
|
|
22
|
-
export function alignChanges(
|
|
26
|
+
export function alignChanges(revision, part, reach, tool) {
|
|
23
27
|
if (!reach.translate)
|
|
24
28
|
return [];
|
|
25
|
-
const box = boxOf(
|
|
29
|
+
const box = boxOf(revision, part);
|
|
26
30
|
if (!box)
|
|
27
31
|
return [];
|
|
28
|
-
const value = (id) => asset.designInputs[id] ?? 0;
|
|
32
|
+
const value = (id) => revision.asset.designInputs[id] ?? 0;
|
|
29
33
|
const set = (input, next) => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) });
|
|
30
34
|
const at = (axis) => `${part}.position.${axis}`;
|
|
31
35
|
switch (tool) {
|
|
@@ -34,7 +38,7 @@ export function alignChanges(asset, part, reach, tool) {
|
|
|
34
38
|
case 'centre-z':
|
|
35
39
|
return [set(at('z'), value(at('z')) - box.centre.z)];
|
|
36
40
|
case 'snap': {
|
|
37
|
-
const step = gridStepOf(
|
|
41
|
+
const step = gridStepOf(revision);
|
|
38
42
|
const round = (n) => Math.round(n / step) * step;
|
|
39
43
|
const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))));
|
|
40
44
|
/* A size never snaps to 0 — the part would look gone. */
|
|
@@ -49,16 +53,16 @@ export function alignChanges(asset, part, reach, tool) {
|
|
|
49
53
|
* Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by
|
|
50
54
|
* `duplicate-part` first; this is the position the copy is then given.
|
|
51
55
|
*/
|
|
52
|
-
export function mirrorChanges(
|
|
53
|
-
const box = boxOf(
|
|
56
|
+
export function mirrorChanges(revision, part, copy, axis) {
|
|
57
|
+
const box = boxOf(revision, part);
|
|
54
58
|
if (!box)
|
|
55
59
|
return [];
|
|
56
60
|
const id = `${part}.position.${axis}`;
|
|
57
|
-
return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }];
|
|
61
|
+
return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((revision.asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }];
|
|
58
62
|
}
|
|
59
63
|
/** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */
|
|
60
|
-
export function canMirror(
|
|
61
|
-
const box = reach.translate ? boxOf(
|
|
64
|
+
export function canMirror(revision, part, axis, reach) {
|
|
65
|
+
const box = reach.translate ? boxOf(revision, part) : undefined;
|
|
62
66
|
return !!box && Math.abs(box.centre[axis]) >= 0.5;
|
|
63
67
|
}
|
|
64
68
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureInputChange, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport { derivedOf, type FigureRevision } from './revision.js'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * mirror on X or Z, snap to the grid.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\n/* The plane tool is figure-model's now, by placement (`figurePlaneAlignChanges`); these stay the screen's. */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\n/**\n * Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a\n * command sees it (a fastening keeps a part where it is drawn).\n */\nexport function boxesOf(revision: FigureRevision): FigureWorldBox[] {\n return derivedOf(revision, 'boxes', () => figureOccupiedBoxesOf(revision.geometry, figureWritersOf(revision.asset)))\n}\n\nexport function boxOf(revision: FigureRevision, part: string): FigureWorldBox | undefined {\n return boxesOf(revision).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(revision: FigureRevision): number {\n const boxes = boxesOf(revision)\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(revision, part)\n if (!box) return []\n const value = (id: string) => revision.asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'snap': {\n const step = gridStepOf(revision)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by\n * `duplicate-part` first; this is the position the copy is then given.\n */\nexport function mirrorChanges(revision: FigureRevision, part: string, copy: string, axis: 'x' | 'z'): FigureInputChange[] {\n const box = boxOf(revision, part)\n if (!box) return []\n const id = `${part}.position.${axis}`\n return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((revision.asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }]\n}\n\n/** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */\nexport function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean {\n const box = reach.translate ? boxOf(revision, part) : undefined\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
|
|
@@ -130,6 +130,8 @@ export declare class FigureEditor extends LitElement {
|
|
|
130
130
|
*/
|
|
131
131
|
private said;
|
|
132
132
|
private run;
|
|
133
|
+
/** The bodies drawn with the value in hand, or null when nothing is in hand (or the figure cannot take it). */
|
|
134
|
+
private liveParts;
|
|
133
135
|
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
134
136
|
private drawnParts;
|
|
135
137
|
/**
|
|
@@ -138,11 +140,11 @@ export declare class FigureEditor extends LitElement {
|
|
|
138
140
|
*/
|
|
139
141
|
private fanOf;
|
|
140
142
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* saved angle while one slider moved
|
|
143
|
+
* The figure on screen at its version: what every panel and helper reads (`revision.ts`). Every input as the session
|
|
144
|
+
* holds it now is its `inputs` -- the pose set with the controls included: drawing a value in hand over the saved
|
|
145
|
+
* defaults put every other joint back to its saved angle while one slider moved (user, 2026-09-24).
|
|
144
146
|
*/
|
|
145
|
-
private
|
|
147
|
+
private shown;
|
|
146
148
|
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
147
149
|
private hold;
|
|
148
150
|
private closeSectionEditor;
|
|
@@ -185,7 +187,6 @@ export declare class FigureEditor extends LitElement {
|
|
|
185
187
|
* fastened. Refused, it says why.
|
|
186
188
|
*/
|
|
187
189
|
private keptExtremeWord;
|
|
188
|
-
private planeContact?;
|
|
189
190
|
private planeContactOf;
|
|
190
191
|
private renderPlaneAlign;
|
|
191
192
|
private alignToPlane;
|
|
@@ -201,8 +202,6 @@ export declare class FigureEditor extends LitElement {
|
|
|
201
202
|
private renderRepeatNote;
|
|
202
203
|
private renamePart;
|
|
203
204
|
/** Size response, fastening and motion for the selected part, as authoring commands. */
|
|
204
|
-
private dimsSaid?;
|
|
205
|
-
private dimsOf;
|
|
206
205
|
private previewWordsSaid?;
|
|
207
206
|
private previewWordsNow;
|
|
208
207
|
private renderLinks;
|
|
@@ -228,8 +227,6 @@ export declare class FigureEditor extends LitElement {
|
|
|
228
227
|
* pointer, and the release records one step. The field's number follows too, so the value in hand is always read.
|
|
229
228
|
*/
|
|
230
229
|
private scrub;
|
|
231
|
-
/** The measured range, kept for the revision it was measured at: measuring evaluates the figure at its edges. */
|
|
232
|
-
private range?;
|
|
233
230
|
/** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
|
|
234
231
|
private renderInstanceRange;
|
|
235
232
|
/**
|
|
@@ -280,7 +277,6 @@ export declare class FigureEditor extends LitElement {
|
|
|
280
277
|
private computePreview;
|
|
281
278
|
/** The preview, while it still speaks of what is on screen: this figure, this version, this part. */
|
|
282
279
|
private currentPreview;
|
|
283
|
-
private detachSaid?;
|
|
284
280
|
private detachTimer?;
|
|
285
281
|
private detachAsked?;
|
|
286
282
|
/**
|
|
@@ -289,12 +285,8 @@ export declare class FigureEditor extends LitElement {
|
|
|
289
285
|
*/
|
|
290
286
|
private detachWords;
|
|
291
287
|
private computeDetachWords;
|
|
292
|
-
private
|
|
293
|
-
/**
|
|
294
|
-
private overhangNote;
|
|
295
|
-
private overhangWords;
|
|
296
|
-
/** The selected part's motion at both ends of its range, made once per figure, version and part. */
|
|
297
|
-
private motionEndsSaid?;
|
|
288
|
+
private overhangOf;
|
|
289
|
+
/** The selected part's motion at both ends of its range, made once per version and part. */
|
|
298
290
|
private motionEnds;
|
|
299
291
|
/** "Fasten it first", from the motion tab: to the properties tab, the fastening form open on this part. */
|
|
300
292
|
private goFasten;
|
|
@@ -313,23 +305,6 @@ export declare class FigureEditor extends LitElement {
|
|
|
313
305
|
};
|
|
314
306
|
};
|
|
315
307
|
private choose;
|
|
316
|
-
/**
|
|
317
|
-
* The figure as the session holds it now, parsed once per text: a render asks for it a dozen times, and parsing the whole
|
|
318
|
-
* figure is most of what a selection costs. Read, never changed -- what changes a figure goes through the session.
|
|
319
|
-
*/
|
|
320
|
-
/**
|
|
321
|
-
* The session has evaluated this figure at this version, with these inputs: the screen's questions of the text it just
|
|
322
|
-
* parsed are answered from that, not by evaluating the figure again (each is tens of milliseconds on a real figure).
|
|
323
|
-
*/
|
|
324
|
-
private seeded;
|
|
325
|
-
private parsed?;
|
|
326
|
-
private parsedAsset;
|
|
327
|
-
private plain?;
|
|
328
|
-
/**
|
|
329
|
-
* The figure as plain data, parsed once per text and frozen: the render, the gizmo's reach and every tool read it, and
|
|
330
|
-
* a copy for each of them cost more than the selection they serve. A change goes through the session, never through this.
|
|
331
|
-
*/
|
|
332
|
-
private plainAsset;
|
|
333
308
|
private asset;
|
|
334
309
|
/** A side tab says what it is in words under its icon: the icons alone had to be learnt (designer 2026-09-27, U05). */
|
|
335
310
|
private renderTab;
|
|
@@ -346,7 +321,6 @@ export declare class FigureEditor extends LitElement {
|
|
|
346
321
|
* Why a drawn push's end cannot be drawn to one point now: the kernel's own refusal of that command on a copy (what is
|
|
347
322
|
* fastened to the end face), said as the face. Asked once per figure revision and part.
|
|
348
323
|
*/
|
|
349
|
-
private pointBlocked?;
|
|
350
324
|
private pointBlockedOf;
|
|
351
325
|
private renderStart;
|
|
352
326
|
/**
|