@things-factory/figure-ui 10.1.204 → 10.1.206
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 +23 -22
- package/client/modeller/editor.ts +217 -184
- package/client/modeller/fasten-form.ts +130 -12
- package/client/modeller/figure-fasten-dialog.ts +45 -11
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-links.ts +2 -2
- package/client/modeller/figure-preview.ts +6 -6
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +22 -1
- package/client/modeller/keys.ts +10 -1
- package/client/modeller/links.ts +79 -46
- package/client/modeller/palette.ts +1 -1
- package/client/modeller/product-session.ts +16 -4
- package/client/modeller/refusal-teller.ts +3 -25
- package/client/modeller/refusals.ts +19 -26
- 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 +23 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +35 -35
- package/dist-client/modeller/editor.js +221 -193
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +46 -4
- package/dist-client/modeller/fasten-form.js +120 -11
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +18 -4
- package/dist-client/modeller/figure-fasten-dialog.js +49 -12
- 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-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +2 -2
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +4 -4
- package/dist-client/modeller/figure-preview.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 +18 -1
- package/dist-client/modeller/gizmo.js +19 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/keys.d.ts +1 -1
- package/dist-client/modeller/keys.js +6 -0
- package/dist-client/modeller/keys.js.map +1 -1
- package/dist-client/modeller/links.d.ts +30 -24
- package/dist-client/modeller/links.js +67 -42
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/palette.js +1 -1
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +15 -13
- package/dist-client/modeller/product-session.js +15 -5
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +1 -12
- package/dist-client/modeller/refusal-teller.js +3 -22
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +6 -14
- package/dist-client/modeller/refusals.js +16 -27
- package/dist-client/modeller/refusals.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/route.d.ts +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 +11 -1
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +124 -69
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +77 -26
- package/test/browser/parts.check.mjs +49 -0
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +29 -2
- package/test/fasten-form.test.ts +102 -1
- package/test/gizmo.test.ts +25 -16
- package/test/keys.test.ts +11 -0
- package/test/links.test.ts +46 -36
- package/test/refusals.test.ts +17 -21
- 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 +32 -34
- package/translations/es.json +32 -34
- package/translations/ja.json +32 -34
- package/translations/ko.json +32 -34
- package/translations/ms.json +32 -34
- package/translations/zh.json +32 -34
package/client/modeller/links.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, figureFollowOf, proposeFigureProportion,
|
|
2
|
-
import type { FigureFollow,
|
|
3
|
-
import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureDimensionSource, FigureFace, FigureFacing, FigureJoint } from '@hatiolab/figure-model'
|
|
1
|
+
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, figureFollowOf, proposeFigureProportion, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeRangeOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf, figureFastenKeptPlaceOf, figureFastenAxesOf, figureJointAsFixed, figureJointWithMotion, figureSurfaceOf } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureFollow, FigureInstanceSizeRange } from '@hatiolab/figure-model'
|
|
3
|
+
import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureDimensionSource, FigureFace, FigureFacing, FigureJoint, FigureSurfacePlace } from '@hatiolab/figure-model'
|
|
4
4
|
import { startingShapeKindOf } from './starting-shape.js'
|
|
5
5
|
import { boxOf } from './align.js'
|
|
6
6
|
import { graphValuesOf, type FigureRevision } from './revision.js'
|
|
@@ -35,6 +35,23 @@ export function followOf(asset: FigureAsset, part: string, axis: Axis): Follow {
|
|
|
35
35
|
return figureFollowOf(asset, part, axis)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The parts whose size follows the figure's size on some axis, in the model's order: the ones that change when the
|
|
40
|
+
* figure is resized on a drawing. Read by figure-model's own follow (`figureFollowOf`), not worked out again here.
|
|
41
|
+
*/
|
|
42
|
+
export function sizeFollowersOf(asset: FigureAsset): string[] {
|
|
43
|
+
const parts = (modelOf(asset).nodes as Array<{ id: string; op: string }>).filter(n => n.op === 'place@1').map(n => n.id)
|
|
44
|
+
return parts.filter(part =>
|
|
45
|
+
AXES.some(axis => {
|
|
46
|
+
try {
|
|
47
|
+
return figureFollowOf(asset, part, axis).of === 'instance'
|
|
48
|
+
} catch {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
/** Whether the asset declares an instance size on an axis — only then can a dimension follow it. */
|
|
39
56
|
export const hasInstanceSize = (asset: FigureAsset, axis: Axis) => modelOf(asset).inputs.some(i => i.id === `size.${axis}` && i.role === 'design')
|
|
40
57
|
|
|
@@ -224,7 +241,14 @@ export function measureNow(revision: FigureRevision, part: string, axis: Axis):
|
|
|
224
241
|
*/
|
|
225
242
|
export function facesOf(asset: FigureAsset, part: string): string[] {
|
|
226
243
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
227
|
-
|
|
244
|
+
const faces = (shape && figureShapeFaces(shape as never)) ?? {}
|
|
245
|
+
/* A cylinder's curved side is no face: a part is fastened to a point on it (ADR-0140 3.9, `surfaceOf`). */
|
|
246
|
+
return inScreenOrder(Object.keys(faces).filter(f => faces[f]!.contact !== 'line'))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The curved surface a part offers to be fastened to at a point: a cylinder's side or a sphere's round (ADR-0140 3.9). */
|
|
250
|
+
export function surfaceOf(asset: FigureAsset, part: string): { form: 'around' | 'toward'; axis: Axis } | undefined {
|
|
251
|
+
return figureSurfaceOf(asset, part)
|
|
228
252
|
}
|
|
229
253
|
|
|
230
254
|
/** Faces in the order the screen lists them: the six outer ones left to front, then the rest as the shape gives them. */
|
|
@@ -242,7 +266,7 @@ export function faceAxisOf(asset: FigureAsset, part: string, face: string): Axis
|
|
|
242
266
|
|
|
243
267
|
/** Whether `attach` can take this part: any part whose shape has a flat face. */
|
|
244
268
|
export function canFasten(asset: FigureAsset, part: string): boolean {
|
|
245
|
-
return facesOf(asset, part).length > 0
|
|
269
|
+
return facesOf(asset, part).length > 0 || !!surfaceOf(asset, part)
|
|
246
270
|
}
|
|
247
271
|
|
|
248
272
|
const OUTER: Record<Axis, [string, string]> = { x: ['left', 'right'], y: ['bottom', 'top'], z: ['back', 'front'] }
|
|
@@ -267,13 +291,15 @@ export function rowAxesOf(asset: FigureAsset, part: string): Axis[] {
|
|
|
267
291
|
* Why a part cannot be made into a row here, in the terms `rowAxesOf` refuses it: said, not hidden (ADR-0097 §5.5).
|
|
268
292
|
* Nothing when it can, or when it already is a row (that row is shown where it stands).
|
|
269
293
|
*/
|
|
270
|
-
export function rowBlockOf(asset: FigureAsset, part: string): 'unfastened' | 'on-plane' | 'holds-parts' | 'at-origin' | undefined {
|
|
294
|
+
export function rowBlockOf(asset: FigureAsset, part: string): 'unfastened' | 'on-plane' | 'holds-parts' | 'at-origin' | 'on-surface' | undefined {
|
|
271
295
|
const joints = asset.joints ?? []
|
|
272
296
|
const j = joints.find(o => o.body1.part === part)
|
|
273
297
|
if (!j) return 'unfastened'
|
|
274
298
|
if (j.repeat) return undefined
|
|
275
299
|
if (!('part' in j.body0)) return 'on-plane'
|
|
276
300
|
if (joints.some(o => 'part' in o.body0 && o.body0.part === part)) return 'holds-parts'
|
|
301
|
+
/* On a point of a curved surface a row has no face to run across (ADR-0140 3.9): the kernel refuses it. */
|
|
302
|
+
if ('surface' in j.body0) return 'on-surface'
|
|
277
303
|
if (!('face' in j.body0) || 'at' in j.body1) return 'at-origin'
|
|
278
304
|
return undefined
|
|
279
305
|
}
|
|
@@ -372,13 +398,29 @@ export function fasteningOf(
|
|
|
372
398
|
alignMm: Partial<Record<Axis, number>>
|
|
373
399
|
/** Placed by a part's origin (ADR-0107 decision 4): no face to fasten by, and the form does not write it. */
|
|
374
400
|
atOrigin?: true
|
|
401
|
+
/** On a point of the target's curved surface (ADR-0140 3.9), as the record says it. */
|
|
402
|
+
surface?: FigureSurfacePlace
|
|
375
403
|
} | null {
|
|
376
404
|
const record = revision.asset.joints?.find(j => j.body1.part === part)
|
|
377
405
|
if (!record) return null
|
|
378
406
|
const toPlane = 'plane' in record.body0
|
|
379
407
|
const target = 'part' in record.body0 ? record.body0.part : ''
|
|
408
|
+
const surface = 'surface' in record.body0 ? record.body0.surface : undefined
|
|
380
409
|
const targetFace = 'face' in record.body0 ? record.body0.face : 'top'
|
|
381
|
-
const atOrigin = !('plane' in record.body0) && (!('face' in record.body0) || 'at' in record.body1)
|
|
410
|
+
const atOrigin = !('plane' in record.body0) && ((!('face' in record.body0) && !surface) || 'at' in record.body1)
|
|
411
|
+
if (surface)
|
|
412
|
+
return {
|
|
413
|
+
toPlane,
|
|
414
|
+
target,
|
|
415
|
+
targetFace,
|
|
416
|
+
facing: record.origin?.facing ?? 'meet',
|
|
417
|
+
gap: typeof record.origin?.gap === 'number' ? record.origin.gap : 0,
|
|
418
|
+
...(record.origin?.gap !== undefined && typeof record.origin.gap !== 'number' ? { gapKept: record.origin.gap } : {}),
|
|
419
|
+
align: {},
|
|
420
|
+
alignMm: {},
|
|
421
|
+
surface,
|
|
422
|
+
...(atOrigin ? { atOrigin: true as const } : {})
|
|
423
|
+
}
|
|
382
424
|
/* Where across the face: as the record says it, and where it says nothing, where the part stands now. */
|
|
383
425
|
const kept = placeKeptOf(revision, part, toPlane ? 'plane' : { part: target, face: targetFace })
|
|
384
426
|
for (const [axis, how] of Object.entries(record.origin?.align ?? {}) as Array<[Axis, unknown]>) {
|
|
@@ -691,53 +733,48 @@ export const holdsThings = (asset: FigureAsset) => (asset.document.capabilities.
|
|
|
691
733
|
export const acceptsOf = (typed: string) => [...new Set(typed.split(',').map(one => one.trim()).filter(Boolean))]
|
|
692
734
|
|
|
693
735
|
/**
|
|
694
|
-
* The sizes a board may give this figure: the
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const declared = {} as FigureSizeRange
|
|
702
|
-
const authored = {} as Record<Axis, number>
|
|
703
|
-
for (const axis of AXES) {
|
|
704
|
-
const input = inputs.find(i => i.id === `size.${axis}` && i.role === 'design')
|
|
705
|
-
const at = asset.designInputs[`size.${axis}`]
|
|
706
|
-
if (!input || typeof at !== 'number') return undefined
|
|
707
|
-
declared[axis] = { min: input.min ?? 0, max: input.max ?? Infinity }
|
|
708
|
-
authored[axis] = at
|
|
709
|
-
}
|
|
710
|
-
const start = defaultInstanceSizeRange(authored)
|
|
711
|
-
const measured = foldInstanceSizeRange(start, figureInstanceSizeLimitsOf(asset, start)).range
|
|
712
|
-
return { declared, measured }
|
|
736
|
+
* The sizes a board may give this figure, per axis: the ends its author narrowed, and the rest as far as its parts
|
|
737
|
+
* allow, computed by figure-model (`figureInstanceSizeRangeOf`, ADR-0087 decision 6) -- the screen does not measure
|
|
738
|
+
* on its own. Nothing when no size is declared.
|
|
739
|
+
*/
|
|
740
|
+
export function instanceRangeOf(asset: FigureAsset): Record<Axis, FigureInstanceSizeRange> | undefined {
|
|
741
|
+
const range = figureInstanceSizeRangeOf(asset)
|
|
742
|
+
return AXES.every(axis => range[axis]) ? (range as Record<Axis, FigureInstanceSizeRange>) : undefined
|
|
713
743
|
}
|
|
714
744
|
|
|
715
745
|
/**
|
|
716
|
-
* The size
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
* to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
|
|
746
|
+
* The size to declare for a figure that has none yet: its parts' extent now, the size it is authored at. Undefined when
|
|
747
|
+
* the figure already has a size or has nothing to measure. Without a declared size a board cannot resize the figure,
|
|
748
|
+
* and no part is offered to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
|
|
720
749
|
*/
|
|
721
|
-
export function
|
|
750
|
+
export function instanceSizeToDeclare(asset: FigureAsset): { at: Record<Axis, number> } | undefined {
|
|
722
751
|
if (instanceRangeOf(asset) || !asset.appearance.length) return undefined
|
|
723
752
|
/* The parts as they rest -- not the room their joints swing them through, which declared a door 8208 mm every way. */
|
|
724
753
|
const at = figurePartsExtentOf(asset) as Record<Axis, number>
|
|
725
754
|
if (AXES.some(axis => !(at[axis] > 0))) return undefined
|
|
726
|
-
|
|
727
|
-
return { range: foldInstanceSizeRange(start, figureInstanceSizeLimitsOf(asset, start)).range, at }
|
|
755
|
+
return { at }
|
|
728
756
|
}
|
|
729
757
|
|
|
730
|
-
/** Whether measuring again would change the declared range (to a micrometre). */
|
|
731
758
|
/**
|
|
732
|
-
* The
|
|
733
|
-
*
|
|
734
|
-
* value is not a number; whether the figure holds there is the command's to say.
|
|
759
|
+
* The ends the author narrowed, with one end typed or cleared: a number narrows that end, an empty field opens it
|
|
760
|
+
* again to what the parts allow. Whether the figure holds there is the command's to say.
|
|
735
761
|
*/
|
|
736
|
-
export function rangeWith(
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
762
|
+
export function rangeWith(
|
|
763
|
+
range: Record<Axis, FigureInstanceSizeRange>,
|
|
764
|
+
axis: Axis,
|
|
765
|
+
bound: 'min' | 'max',
|
|
766
|
+
value: number
|
|
767
|
+
): Partial<Record<Axis, { min?: number; max?: number }>> {
|
|
768
|
+
const out: Partial<Record<Axis, { min?: number; max?: number }>> = {}
|
|
769
|
+
for (const a of AXES) {
|
|
770
|
+
const ends: { min?: number; max?: number } = {}
|
|
771
|
+
for (const b of ['min', 'max'] as const) {
|
|
772
|
+
const typed = a === axis && b === bound
|
|
773
|
+
if (typed ? Number.isFinite(value) : range[a].narrowed[b]) ends[b] = typed ? value : range[a][b]
|
|
774
|
+
}
|
|
775
|
+
if (Object.keys(ends).length) out[a] = ends
|
|
776
|
+
}
|
|
777
|
+
return out
|
|
741
778
|
}
|
|
742
779
|
|
|
743
780
|
/**
|
|
@@ -765,10 +802,6 @@ export function valueNow(revision: FigureRevision, ref: string | undefined): num
|
|
|
765
802
|
}
|
|
766
803
|
}
|
|
767
804
|
|
|
768
|
-
export function rangeDiffers(a: FigureSizeRange, b: FigureSizeRange): boolean {
|
|
769
|
-
return AXES.some(axis => Math.abs(a[axis].min - b[axis].min) > 1e-3 || Math.abs(a[axis].max - b[axis].max) > 1e-3)
|
|
770
|
-
}
|
|
771
|
-
|
|
772
805
|
/**
|
|
773
806
|
* Whether the figure is a mechanism -- a part turns about a joint. Such a figure should grow by one ratio (chief
|
|
774
807
|
* architect's ruling 2026-09-24); the modeller proposes it, the author declares it.
|
|
@@ -16,7 +16,7 @@ export function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimit
|
|
|
16
16
|
case 'rounded-box':
|
|
17
17
|
return { ...standing(100), kind, dimensions: { width: 100, height: 100, depth: 100, round: 10 } }
|
|
18
18
|
case 'sphere':
|
|
19
|
-
return { ...standing(100), kind, dimensions: {
|
|
19
|
+
return { ...standing(100), kind, dimensions: { radius: 50, radiusY: 50 } }
|
|
20
20
|
case 'frustum':
|
|
21
21
|
return { ...standing(100), kind, dimensions: { radiusTop: 0, radiusBottom: 50, height: 100 } }
|
|
22
22
|
case 'polygon':
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
createFigureEditorWorkspace,
|
|
6
6
|
serializeFigureAsset,
|
|
7
7
|
parseFigureAsset,
|
|
8
|
+
figureSizeRangeFitsOf,
|
|
9
|
+
type FigureSizeRangeFit,
|
|
8
10
|
type FigureInputChange,
|
|
9
11
|
type FigurePrimitiveCreation,
|
|
10
12
|
type FigurePartEdit,
|
|
@@ -71,10 +73,14 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
71
73
|
*/
|
|
72
74
|
let done: Array<string | undefined> = [],
|
|
73
75
|
undone: Array<string | undefined> = []
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
/* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */
|
|
77
|
+
let fitted: FigureSizeRangeFit[] = []
|
|
78
|
+
function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string, setsRange = false): T {
|
|
79
|
+
const was = workspace.read()
|
|
76
80
|
const result = run()
|
|
77
|
-
|
|
81
|
+
const now = workspace.read()
|
|
82
|
+
if (now.revision === was.revision) return result
|
|
83
|
+
fitted = kind === 'do' && !setsRange && was.asset && now.asset ? figureSizeRangeFitsOf(was.asset, now.asset) : []
|
|
78
84
|
if (kind === 'do') {
|
|
79
85
|
done.push(tag)
|
|
80
86
|
undone = []
|
|
@@ -250,7 +256,13 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
250
256
|
},
|
|
251
257
|
/** One authoring command — what a person means, not a graph edit — as one step of the same history. */
|
|
252
258
|
command(action: FigureAuthoringAction) {
|
|
253
|
-
return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }))
|
|
259
|
+
return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }), undefined, action.kind === 'declare-instance-size')
|
|
260
|
+
},
|
|
261
|
+
/** The ends of the size on a drawing the last step moved in to what the parts allow, once: then nothing until the next. */
|
|
262
|
+
takeSizeRangeFits(): FigureSizeRangeFit[] {
|
|
263
|
+
const out = fitted
|
|
264
|
+
fitted = []
|
|
265
|
+
return out
|
|
254
266
|
},
|
|
255
267
|
/**
|
|
256
268
|
* The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and
|
|
@@ -2,29 +2,23 @@ import type { PropertyValues } from 'lit'
|
|
|
2
2
|
import { i18next } from '@operato/i18n'
|
|
3
3
|
import { notify } from '@operato/layout'
|
|
4
4
|
import { OxPrompt } from '@operato/popup'
|
|
5
|
-
import type {
|
|
5
|
+
import type { FigureClipLoss } from '@hatiolab/figure-model'
|
|
6
6
|
import type { FigureProductSession } from './product-session.js'
|
|
7
7
|
import { clipLossOf } from './refusals.js'
|
|
8
8
|
|
|
9
|
-
/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
|
|
10
|
-
export type Retry = { label: string; action?: FigureAuthoringAction; run?: () => void }
|
|
11
|
-
|
|
12
9
|
/** What the teller reads of the editor, and what it asks of it. */
|
|
13
10
|
export interface RefusalHost {
|
|
14
11
|
session(): FigureProductSession | undefined
|
|
15
12
|
error(): string
|
|
16
13
|
notice(): string
|
|
17
|
-
retry(): Retry | undefined
|
|
18
14
|
/** Said as the editor's refusal. */
|
|
19
15
|
refuse(message: string): void
|
|
20
|
-
/** A narrowed link, confirmed: run as the editor runs a command. */
|
|
21
|
-
command(action: FigureAuthoringAction): void
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
/**
|
|
25
19
|
* Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
|
|
26
20
|
* notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
|
|
27
|
-
* delete and fit the space again,
|
|
21
|
+
* delete and fit the space again, let a clip go -- asks in the app's prompt and goes on when confirmed; any
|
|
28
22
|
* other refusal is the app's notification, which stays until it is closed.
|
|
29
23
|
*/
|
|
30
24
|
export class RefusalTeller {
|
|
@@ -35,11 +29,7 @@ export class RefusalTeller {
|
|
|
35
29
|
/** After the editor's update: what changed in its refusal and notice, and the session's own error, said once. */
|
|
36
30
|
tell(changed: PropertyValues) {
|
|
37
31
|
const error = this.of.error()
|
|
38
|
-
if (changed.has('error') && error) {
|
|
39
|
-
const retry = this.of.retry()
|
|
40
|
-
if (retry) this.ask(error, retry)
|
|
41
|
-
else notify({ level: 'error', message: error })
|
|
42
|
-
}
|
|
32
|
+
if (changed.has('error') && error) notify({ level: 'error', message: error })
|
|
43
33
|
const notice = this.of.notice()
|
|
44
34
|
if (changed.has('notice') && notice) notify({ level: 'info', message: notice })
|
|
45
35
|
const viewError = this.of.session()?.read()?.error ?? ''
|
|
@@ -91,16 +81,4 @@ export class RefusalTeller {
|
|
|
91
81
|
}
|
|
92
82
|
asked.session.withConsent({ dropClips: clips.map(c => c.id), context: asked.context, revision: asked.revision }, again)
|
|
93
83
|
}
|
|
94
|
-
|
|
95
|
-
private async ask(message: string, retry: Retry) {
|
|
96
|
-
const agreed = await OxPrompt.open({
|
|
97
|
-
type: 'question',
|
|
98
|
-
text: message,
|
|
99
|
-
confirmButton: { text: retry.label },
|
|
100
|
-
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
101
|
-
})
|
|
102
|
-
if (!agreed) return
|
|
103
|
-
if (retry.run) retry.run()
|
|
104
|
-
else this.of.command(retry.action!)
|
|
105
|
-
}
|
|
106
84
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* English is for logs and readers of the kernel, and a screen that read it went quiet whenever a sentence changed or a
|
|
8
8
|
* new one came under the same code (ADR-0054). A refusal with no details the screen knows is said with its code.
|
|
9
9
|
*/
|
|
10
|
-
import { FIGURE_INSTANCE_SIZE_INPUTS, type FigureClipLoss, type FigureRefusalDetails } from '@hatiolab/figure-model'
|
|
10
|
+
import { FIGURE_INSTANCE_SIZE_INPUTS, type FigureClipLoss, type FigureRefusalDetails, type FigureSizeRangeFit } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
type Say = (key: string, params: Record<string, string>) => string
|
|
13
13
|
type Reason = FigureRefusalDetails['reason']
|
|
@@ -23,36 +23,29 @@ const numberOf = (value: number) => value.toLocaleString(undefined, { maximumFra
|
|
|
23
23
|
/** Bytes in KiB, rounded up: a figure a few bytes over its limit is not said to be at it. */
|
|
24
24
|
const kibOf = (bytes: number) => Math.ceil(bytes / 1024).toLocaleString()
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The ends an author typed for the size on a drawing, refused: they leave out the size the figure is drawn at, or start
|
|
28
|
+
* at zero. The only refusals the size range has left (ADR-0087 decision 6); nothing for any other refusal.
|
|
29
|
+
*/
|
|
27
30
|
export function sizeRangeRefusalOf(error: unknown, say: Say): string | null {
|
|
28
31
|
if (codeOf(error) !== 'INSTANCE_SIZE_RANGE') return null
|
|
29
|
-
const reach = detailsOf(error, 'size-cannot-reach')
|
|
30
|
-
if (reach)
|
|
31
|
-
return say(reach.bound === 'min' ? 'figure.text.size-range-too-small' : 'figure.text.size-range-too-large', { axis: reach.axis.toUpperCase(), mm: mmOf(reach.need), part: reach.part ?? '' })
|
|
32
32
|
const outside = detailsOf(error, 'size-outside-range')
|
|
33
|
-
if (outside)
|
|
33
|
+
if (outside)
|
|
34
|
+
return say('figure.text.size-range-leaves-out-size', {
|
|
35
|
+
axis: say(`figure.label.resize-${outside.axis}`, {}),
|
|
36
|
+
range: `${mmOf(outside.min ?? 0)} – ${outside.max === undefined ? '∞' : mmOf(outside.max)}`,
|
|
37
|
+
at: mmOf(outside.at)
|
|
38
|
+
})
|
|
34
39
|
const zero = detailsOf(error, 'size-min-not-positive')
|
|
35
|
-
if (zero) return say('figure.text.size-range-from-zero', { axis: zero.axis
|
|
36
|
-
const gone = detailsOf(error, 'size-already-gone')
|
|
37
|
-
if (gone) return say('figure.text.size-range-part-gone', { axis: gone.axis.toUpperCase(), at: mmOf(gone.at) })
|
|
40
|
+
if (zero) return say('figure.text.size-range-from-zero', { axis: say(`figure.label.resize-${zero.axis}`, {}) })
|
|
38
41
|
return null
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (action.kind !== 'link-dimension' || codeOf(error) !== 'INSTANCE_SIZE_RANGE') return null
|
|
47
|
-
const reach = detailsOf(error, 'size-cannot-reach')
|
|
48
|
-
if (!reach) return null
|
|
49
|
-
const words = { axis: reach.axis.toUpperCase(), mm: mmOf(reach.need), part: reach.part ?? '' }
|
|
50
|
-
const below = reach.bound === 'min'
|
|
51
|
-
return {
|
|
52
|
-
message: say(below ? 'figure.text.link-needs-smaller-range' : 'figure.text.link-needs-larger-range', words),
|
|
53
|
-
label: say(below ? 'figure.button.narrow-range-from' : 'figure.button.narrow-range-to', words),
|
|
54
|
-
retry: { ...action, narrowRange: true }
|
|
55
|
-
}
|
|
44
|
+
/** What a command did to the ends the author typed, in one line each: what moved, from what to what. */
|
|
45
|
+
export function sizeRangeFitsOf(fits: readonly FigureSizeRangeFit[], say: Say): string {
|
|
46
|
+
return fits
|
|
47
|
+
.map(f => say(`figure.text.size-range-moved-${f.bound}`, { axis: say(`figure.label.resize-${f.axis}`, {}), from: mmOf(f.from), to: mmOf(f.to) }))
|
|
48
|
+
.join(' ')
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
/**
|
|
@@ -297,7 +290,7 @@ export function dataMotionRefusalOf(error: unknown, say: Say): string | null {
|
|
|
297
290
|
const setting = detailsOf(error, 'motion-setting')
|
|
298
291
|
if (setting) return say('figure.text.motion-setting-' + setting.why, { group: setting.group, clip: setting.clip ?? '' })
|
|
299
292
|
const key = codeOf(error) === 'MOTION_GROUP' ? detailsOf(error, 'value-outside-range') : undefined
|
|
300
|
-
if (key) return say('figure.text.motion-key-outside-range', { input: key.input, value: numberOf(key.value), min: numberOf(key.min), max: numberOf(key.max) })
|
|
293
|
+
if (key) return say('figure.text.motion-key-outside-range', { input: key.input, value: numberOf(key.value), min: numberOf(key.min!), max: numberOf(key.max!) })
|
|
301
294
|
return null
|
|
302
295
|
}
|
|
303
296
|
|
|
@@ -310,7 +303,7 @@ export function inputRangeRefusalOf(error: unknown, say: Say): string | null {
|
|
|
310
303
|
const range = detailsOf(error, 'value-outside-range')
|
|
311
304
|
if (!range) return null
|
|
312
305
|
const axis = (Object.entries(FIGURE_INSTANCE_SIZE_INPUTS) as Array<[string, string]>).find(([, id]) => id === range.input)?.[0]
|
|
313
|
-
const params = { value: numberOf(range.value), min: numberOf(range.min), max: numberOf(range.max) }
|
|
306
|
+
const params = { value: numberOf(range.value), min: range.min === undefined ? '−∞' : numberOf(range.min), max: range.max === undefined ? '∞' : numberOf(range.max) }
|
|
314
307
|
return axis ? say('figure.text.input-range-size', { ...params, axis: axis.toUpperCase() }) : say('figure.text.input-range', params)
|
|
315
308
|
}
|
|
316
309
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { FIGURE_SECTION_PLANE, FIGURE_SECTION_POINT_LIMIT, checkFigureSection, figureExtrusionFeaturesOf, figureSectionSidesOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape, FigureSection, FigureSectionIssue, FigureSectionMeasure, FigureSectionPoint } from '@hatiolab/figure-model'
|
|
3
|
-
import { END_FACE } from './section-controls.js'
|
|
4
3
|
|
|
5
4
|
/*
|
|
6
5
|
* What the section editor does to a drawn section's points, and how it says what is wrong with them (ADR-0100 §3.2).
|
|
@@ -218,14 +217,15 @@ export function sectionActionOf(part: string, kind: FigureSection['kind'] | unde
|
|
|
218
217
|
}
|
|
219
218
|
|
|
220
219
|
/**
|
|
221
|
-
* What a measure's field says under it: that it follows another value, or
|
|
222
|
-
*
|
|
220
|
+
* What a measure's field says under it: that it follows another value, or what a measure typed keeps -- the part's
|
|
221
|
+
* middle, or its fastening (ADR-0140 4.1, figure-model's `figureSizeEditOf`); null when there is nothing to say.
|
|
223
222
|
*/
|
|
224
|
-
export function measureNoteOf(measure: FigureSectionMeasure | undefined, kept:
|
|
223
|
+
export function measureNoteOf(measure: FigureSectionMeasure | undefined, kept: 'centre' | 'relation' | 'none' | undefined): { key: string } | null {
|
|
225
224
|
if (measure === 'linked') return { key: 'figure.text.section-editor-linked' }
|
|
226
|
-
if (kept ===
|
|
227
|
-
if (kept ===
|
|
228
|
-
return { key: 'figure.text.
|
|
225
|
+
if (kept === 'centre') return { key: 'figure.text.size-keeps-centre' }
|
|
226
|
+
if (kept === 'relation') return { key: 'figure.text.size-keeps-fastening' }
|
|
227
|
+
if (kept === 'none') return { key: 'figure.text.size-keeps-none' }
|
|
228
|
+
return null
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
/** The words beside a drawn section's edit button: how many points it has, and the apex when its end is one point. */
|
|
@@ -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}
|