@things-factory/figure-ui 10.1.208 → 10.1.211
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/data-interface.ts +5 -5
- package/client/modeller/editor.ts +97 -88
- package/client/modeller/fasten-geometry.ts +2 -1
- package/client/modeller/figure-links.ts +87 -157
- package/client/modeller/figure-size-editor.ts +233 -0
- package/client/modeller/joint-drag.ts +25 -21
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +87 -36
- package/client/modeller/live.ts +33 -0
- package/client/modeller/motion-ends.ts +10 -6
- package/client/modeller/size-response.ts +95 -0
- package/dist-client/modeller/data-interface.d.ts +1 -1
- package/dist-client/modeller/data-interface.js +5 -5
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +14 -1
- package/dist-client/modeller/editor.js +103 -89
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.js +1 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -4
- package/dist-client/modeller/figure-links.js +91 -145
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +29 -0
- package/dist-client/modeller/figure-size-editor.js +262 -0
- package/dist-client/modeller/figure-size-editor.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +1 -1
- package/dist-client/modeller/joint-drag.js +27 -22
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/joint-fan.js +4 -3
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +21 -9
- package/dist-client/modeller/links.js +85 -35
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +14 -0
- package/dist-client/modeller/live.js +31 -0
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +4 -1
- package/dist-client/modeller/motion-ends.js +10 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/size-response.d.ts +29 -0
- package/dist-client/modeller/size-response.js +99 -0
- package/dist-client/modeller/size-response.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/evidence/2026-10-24/current-size-panel.png +0 -0
- package/docs/evidence/2026-10-24/integrated-size.png +0 -0
- package/docs/evidence/2026-10-24/multipart-response.png +0 -0
- package/docs/evidence/2026-10-24/repeat-layout.png +0 -0
- package/docs/evidence/2026-10-24/repeat-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/size-in-layout.png +0 -0
- package/docs/evidence/2026-10-24/size-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/width-preview.png +0 -0
- package/docs/multipart-response-review-2026-10-24.md +77 -0
- package/docs/response-authoring-2026-10-24.md +46 -0
- package/docs/size-and-repeat-ux-2026-10-24.md +67 -0
- package/package.json +7 -7
- package/test/attachment-size-contract.test.ts +47 -0
- package/test/browser/acceptance-page.js +1 -1
- package/test/browser/detail-page.js +1 -1
- package/test/browser/detail-tabs-page.js +1 -1
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +66 -14
- package/test/browser/links.check.mjs +41 -22
- package/test/browser/modeller-page.js +1 -1
- package/test/browser/modeller-revision.check.mjs +21 -7
- package/test/browser/modeller.check.mjs +106 -39
- package/test/browser/multipart-response-page.js +8 -0
- package/test/browser/parts.check.mjs +2 -1
- package/test/browser/product-page.js +1 -1
- package/test/browser/props.check.mjs +17 -14
- package/test/browser/response-page.js +12 -0
- package/test/browser/row-preview.check.mjs +3 -3
- package/test/browser/screen.check.mjs +57 -13
- package/test/browser/serve-response.mjs +24 -0
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +2 -2
- package/test/command-preview.test.ts +7 -7
- package/test/joint-drag.test.ts +6 -6
- package/test/joint-frames.test.ts +33 -32
- package/test/joints.ts +1 -6
- package/test/links.test.ts +60 -20
- package/test/multipart-response-fixture.js +17 -0
- package/test/multipart-response.test.ts +37 -0
- package/test/refusals.test.ts +2 -2
- package/test/resize-with-placement.test.ts +23 -1
- package/test/size-response.test.ts +49 -0
- package/test/size-source-pairing.test.ts +66 -0
- package/translations/en.json +47 -10
- package/translations/es.json +47 -10
- package/translations/ja.json +47 -10
- package/translations/ko.json +52 -15
- package/translations/ms.json +47 -10
- package/translations/zh.json +47 -10
|
@@ -97,7 +97,7 @@ export function overhangOf(view, at) {
|
|
|
97
97
|
return { touching: o.over, contact: o.contact, sides: o.sides };
|
|
98
98
|
}
|
|
99
99
|
/** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */
|
|
100
|
-
export const turnsInPlace = (asset, part) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || j.origin?.facing === 'centre'));
|
|
100
|
+
export const turnsInPlace = (asset, part) => (asset.joints ?? []).some(j => j.body1.part === part && ((j.motion ?? []).some(a => a.type === 'revolute') || j.origin?.facing === 'centre'));
|
|
101
101
|
/**
|
|
102
102
|
* How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up
|
|
103
103
|
* for Y, which rises, so a front face is seen as it stands and not upside down.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fasten-geometry.js","sourceRoot":"","sources":["../../client/modeller/fasten-geometry.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureFaceInsideAlignOf, figureFaceMeasureOf, figureFaceOverhangAt, figureFastenPlaceOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureFaceMeasure, FigureFacing, FigureFastenAlignment } from '@hatiolab/figure-model'\n\nimport { alignAxesOf, type Alignment } from './links.js'\nimport type { FigureRevision } from './revision.js'\n\n/*\n * What the 붙이기 drawing reads (user 2026-10-01): the face a part is fastened to, seen from above, and where the part\n * lies on it. A fastening is chosen by the target's face and how (올려놓기 · 같은 면에 맞추기 · 가운데를 면에); the part\n * meets it by its real shape in the pose it has, by no face of its own (ADR-0140 3.2). What is measured -- the face,\n * the part's extent across it, how far it comes past and whether what touches is on it -- is the kernel's\n * (`figureFaceMeasureOf`, 3.8); this only draws it.\n */\n\ntype Axis = 'x' | 'y' | 'z'\nexport type FastenMode = FigureFacing\nexport type FastenTarget = { part: string; face: string } | 'plane'\n\n/**\n * The face seen from above: its two axes across (as the form's alignment names them), the face's size on each as a\n * fastening lines up to it -- none for a plane with no declared size -- the part's real extent on each, whether its\n * edges can be lined up with, and the kernel's measure the drawing asks again as the part is moved.\n */\nexport interface FaceView {\n axes: [Axis, Axis]\n face: Partial<Record<Axis, number>>\n part: Record<Axis, number>\n edges: Record<Axis, boolean>\n measure: FigureFaceMeasure\n}\n\nexport function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget, facing: FastenMode): FaceView | undefined {\n const measure = (() => {\n try {\n return figureFaceMeasureOf(revision.asset, part, revision.inputs, { on: target, facing })\n } catch {\n return undefined\n }\n })()\n if (!measure) return undefined\n const across = alignAxesOf(revision.asset, target)\n const face: Partial<Record<Axis, number>> = {}\n for (const a of measure.axes) {\n const e = measure.edges[a]\n if (e) face[a] = e.hi - e.lo\n }\n const extent = (a: Axis) => measure.extent[a].hi - measure.extent[a].lo\n return {\n axes: measure.axes,\n face,\n part: { x: extent('x'), y: extent('y'), z: extent('z') },\n edges: Object.fromEntries(measure.axes.map(a => [a, !!across.find(o => o.axis === a)?.edges])) as Record<Axis, boolean>,\n measure\n }\n}\n\n/**\n * Where the part's centre is from the face's centre along one axis, for the way it is aligned there: the kernel's\n * `figureFastenPlaceOf` (ADR-0140 D2), the form's distances -- typed, or a share of the size read as millimetres -- as\n * a distance, a position field's as the centre.\n */\nexport function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number {\n const align = how === 'mm' || how === 'share' ? { mm: mm ?? 0 } : how === 'min' || how === 'max' || how === 'centre' ? how : undefined\n return figureFastenPlaceOf({ align: { x: align }, face: { x: face }, part: { x: part } }).x!\n}\n\n/**\n * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:\n * `min` / `max`, where the edges can be lined up with), or at an edge from outside (touching it, as a distance), when\n * within `catchMm`; anywhere else a distance off centre, whole millimetres.\n */\nexport function alignmentAt(d: number, face: number | undefined, part: number, edges: boolean, catchMm: number): { how: Alignment; mm?: number; caught?: 'centre' | 'inside' | 'outside' } {\n if (Math.abs(d) <= catchMm) return { how: 'centre', caught: 'centre' }\n if (face !== undefined) {\n const inside = (face - part) / 2,\n outside = (face + part) / 2\n for (const [at, how] of [\n [-inside, 'min'],\n [inside, 'max']\n ] as const)\n if (Math.abs(d - at) <= catchMm && inside > 0) return edges ? { how, caught: 'inside' } : { how: 'mm', mm: Math.round(at), caught: 'inside' }\n for (const at of [-outside, outside]) if (Math.abs(d - at) <= catchMm) return { how: 'mm', mm: Math.round(at), caught: 'outside' }\n }\n return { how: 'mm', mm: Math.round(d) }\n}\n\n/** The form's alignment on each axis across the face, in the kernel's words: a typed or shared distance as millimetres. */\nfunction kernelAlignOf(view: FaceView, align: Partial<Record<Axis, Alignment>>, alignMm: Partial<Record<Axis, number>>): Partial<Record<Axis, FigureFastenAlignment>> {\n const out: Partial<Record<Axis, FigureFastenAlignment>> = {}\n for (const a of view.axes) {\n const how = align[a] ?? 'centre'\n out[a] = how === 'mm' || how === 'share' ? { mm: alignMm[a] ?? 0 } : how === 'min' || how === 'max' ? how : 'centre'\n }\n return out\n}\n\n/**\n * The alignment that brings the part inside the face, for 정렬·간격 고치기: the kernel's least move\n * (`figureFaceInsideAlignOf`), in the form's words. `inside` when nothing of it is past an edge: what is out is something\n * fastened on it. `unmeasured` when the face has no size to measure against.\n */\nexport function insideAlignmentOf(\n view: FaceView | undefined,\n align: Partial<Record<Axis, Alignment>>,\n alignMm: Partial<Record<Axis, number>>\n): { align: Partial<Record<Axis, Alignment>>; alignMm: Partial<Record<Axis, number>>; moved: Axis[] } | { cannot: 'inside' | 'unmeasured' } {\n if (!view) return { cannot: 'unmeasured' }\n const fix = figureFaceInsideAlignOf(view.measure, kernelAlignOf(view, align, alignMm), view.edges)\n if ('cannot' in fix) return fix\n const next = { align: { ...align }, alignMm: { ...alignMm }, moved: fix.moved as Axis[] }\n for (const a of fix.moved as Axis[]) {\n const how = fix.align[a]\n if (how && typeof how === 'object') ((next.align[a] = 'mm'), (next.alignMm[a] = how.mm))\n else if (how) next.align[a] = how\n }\n return next\n}\n\n/**\n * How far the part comes past the face's edges, per side, whether any of it lies over the face, and whether what\n * touches the face is on it -- with its middle `at` from the face's middle: the kernel's (`figureFaceOverhangAt`).\n */\nexport function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; contact: 'on' | 'off' | 'unknown'; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {\n const o = figureFaceOverhangAt(view.measure, Object.fromEntries(view.axes.map(a => [a, { mm: at[a] }])))\n return { touching: o.over, contact: o.contact, sides: o.sides as Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> }\n}\n\n/** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */\nexport const turnsInPlace = (asset: FigureAsset, part: string) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || j.origin?.facing === 'centre'))\n\n/**\n * How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up\n * for Y, which rises, so a front face is seen as it stands and not upside down.\n */\nexport function screenOf(axes: readonly [Axis, Axis]): { h: Axis; v: Axis; sign: Record<Axis, 1 | -1> } {\n const h = axes.includes('x') ? 'x' : 'z'\n const v = axes.find(a => a !== h)!\n return { h, v, sign: { x: 1, y: 1, z: 1, [v]: v === 'y' ? -1 : 1 } as Record<Axis, 1 | -1> }\n}\n\n/**\n * Where the fastening drawing writes its two names so neither lies over the other (user 2026-10-16: 「roller」 over the\n * roller's thin bar, and 「frame 윗면 2000 × 600」 cut by it). The face's name stands above the face, inside its top edge\n * only when the face fills the drawing. The part's name stands in the part when the part is wide and tall enough for it,\n * else just under it -- above it when the drawing ends first. Under a part as tall as the face, just under it is the\n * face's own edge, and the name lay on that line (2026-10-18, the roller in the fastening window): it goes under the edge.\n * Widths are estimated: 7 px a letter at 11-12 px; a line of it stands 11 px over its baseline and 3 under.\n */\nexport function fastenLabelsOf(\n face: { x: number; y: number; w: number; h: number },\n part: { x: number; y: number; w: number; h: number },\n partName: string,\n height: number\n): { face: { x: number; y: number }; part: { x: number; y: number; inside: boolean } } {\n const facePlace = face.y >= 16 ? { x: face.x, y: face.y - 5 } : { x: face.x + 4, y: face.y + 14 }\n const fits = part.w >= partName.length * 7 + 8 && part.h >= 16\n if (fits) return { face: facePlace, part: { x: part.x + part.w / 2, y: part.y + part.h / 2 + 4, inside: true } }\n const below = part.y + part.h + 14\n const edge = face.y + face.h\n if (below - 11 < edge + 2 && below + 3 > edge - 2 && edge + 14 <= height - 4) return { face: facePlace, part: { x: part.x + part.w / 2, y: edge + 14, inside: false } }\n return { face: facePlace, part: { x: part.x + part.w / 2, y: below <= height - 20 ? below : part.y - 5, inside: false } }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fasten-geometry.js","sourceRoot":"","sources":["../../client/modeller/fasten-geometry.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureFaceInsideAlignOf, figureFaceMeasureOf, figureFaceOverhangAt, figureFastenPlaceOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureFaceMeasure, FigureFacing, FigureFastenAlignment } from '@hatiolab/figure-model'\n\nimport { alignAxesOf, type Alignment } from './links.js'\nimport type { FigureRevision } from './revision.js'\n\n/*\n * What the 붙이기 drawing reads (user 2026-10-01): the face a part is fastened to, seen from above, and where the part\n * lies on it. A fastening is chosen by the target's face and how (올려놓기 · 같은 면에 맞추기 · 가운데를 면에); the part\n * meets it by its real shape in the pose it has, by no face of its own (ADR-0140 3.2). What is measured -- the face,\n * the part's extent across it, how far it comes past and whether what touches is on it -- is the kernel's\n * (`figureFaceMeasureOf`, 3.8); this only draws it.\n */\n\ntype Axis = 'x' | 'y' | 'z'\nexport type FastenMode = FigureFacing\nexport type FastenTarget = { part: string; face: string } | 'plane'\n\n/**\n * The face seen from above: its two axes across (as the form's alignment names them), the face's size on each as a\n * fastening lines up to it -- none for a plane with no declared size -- the part's real extent on each, whether its\n * edges can be lined up with, and the kernel's measure the drawing asks again as the part is moved.\n */\nexport interface FaceView {\n axes: [Axis, Axis]\n face: Partial<Record<Axis, number>>\n part: Record<Axis, number>\n edges: Record<Axis, boolean>\n measure: FigureFaceMeasure\n}\n\nexport function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget, facing: FastenMode): FaceView | undefined {\n const measure = (() => {\n try {\n return figureFaceMeasureOf(revision.asset, part, revision.inputs, { on: target, facing })\n } catch {\n return undefined\n }\n })()\n if (!measure) return undefined\n const across = alignAxesOf(revision.asset, target)\n const face: Partial<Record<Axis, number>> = {}\n for (const a of measure.axes) {\n const e = measure.edges[a]\n if (e) face[a] = e.hi - e.lo\n }\n const extent = (a: Axis) => measure.extent[a].hi - measure.extent[a].lo\n return {\n axes: measure.axes,\n face,\n part: { x: extent('x'), y: extent('y'), z: extent('z') },\n edges: Object.fromEntries(measure.axes.map(a => [a, !!across.find(o => o.axis === a)?.edges])) as Record<Axis, boolean>,\n measure\n }\n}\n\n/**\n * Where the part's centre is from the face's centre along one axis, for the way it is aligned there: the kernel's\n * `figureFastenPlaceOf` (ADR-0140 D2), the form's distances -- typed, or a share of the size read as millimetres -- as\n * a distance, a position field's as the centre.\n */\nexport function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number {\n const align = how === 'mm' || how === 'share' ? { mm: mm ?? 0 } : how === 'min' || how === 'max' || how === 'centre' ? how : undefined\n return figureFastenPlaceOf({ align: { x: align }, face: { x: face }, part: { x: part } }).x!\n}\n\n/**\n * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:\n * `min` / `max`, where the edges can be lined up with), or at an edge from outside (touching it, as a distance), when\n * within `catchMm`; anywhere else a distance off centre, whole millimetres.\n */\nexport function alignmentAt(d: number, face: number | undefined, part: number, edges: boolean, catchMm: number): { how: Alignment; mm?: number; caught?: 'centre' | 'inside' | 'outside' } {\n if (Math.abs(d) <= catchMm) return { how: 'centre', caught: 'centre' }\n if (face !== undefined) {\n const inside = (face - part) / 2,\n outside = (face + part) / 2\n for (const [at, how] of [\n [-inside, 'min'],\n [inside, 'max']\n ] as const)\n if (Math.abs(d - at) <= catchMm && inside > 0) return edges ? { how, caught: 'inside' } : { how: 'mm', mm: Math.round(at), caught: 'inside' }\n for (const at of [-outside, outside]) if (Math.abs(d - at) <= catchMm) return { how: 'mm', mm: Math.round(at), caught: 'outside' }\n }\n return { how: 'mm', mm: Math.round(d) }\n}\n\n/** The form's alignment on each axis across the face, in the kernel's words: a typed or shared distance as millimetres. */\nfunction kernelAlignOf(view: FaceView, align: Partial<Record<Axis, Alignment>>, alignMm: Partial<Record<Axis, number>>): Partial<Record<Axis, FigureFastenAlignment>> {\n const out: Partial<Record<Axis, FigureFastenAlignment>> = {}\n for (const a of view.axes) {\n const how = align[a] ?? 'centre'\n out[a] = how === 'mm' || how === 'share' ? { mm: alignMm[a] ?? 0 } : how === 'min' || how === 'max' ? how : 'centre'\n }\n return out\n}\n\n/**\n * The alignment that brings the part inside the face, for 정렬·간격 고치기: the kernel's least move\n * (`figureFaceInsideAlignOf`), in the form's words. `inside` when nothing of it is past an edge: what is out is something\n * fastened on it. `unmeasured` when the face has no size to measure against.\n */\nexport function insideAlignmentOf(\n view: FaceView | undefined,\n align: Partial<Record<Axis, Alignment>>,\n alignMm: Partial<Record<Axis, number>>\n): { align: Partial<Record<Axis, Alignment>>; alignMm: Partial<Record<Axis, number>>; moved: Axis[] } | { cannot: 'inside' | 'unmeasured' } {\n if (!view) return { cannot: 'unmeasured' }\n const fix = figureFaceInsideAlignOf(view.measure, kernelAlignOf(view, align, alignMm), view.edges)\n if ('cannot' in fix) return fix\n const next = { align: { ...align }, alignMm: { ...alignMm }, moved: fix.moved as Axis[] }\n for (const a of fix.moved as Axis[]) {\n const how = fix.align[a]\n if (how && typeof how === 'object') ((next.align[a] = 'mm'), (next.alignMm[a] = how.mm))\n else if (how) next.align[a] = how\n }\n return next\n}\n\n/**\n * How far the part comes past the face's edges, per side, whether any of it lies over the face, and whether what\n * touches the face is on it -- with its middle `at` from the face's middle: the kernel's (`figureFaceOverhangAt`).\n */\nexport function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; contact: 'on' | 'off' | 'unknown'; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {\n const o = figureFaceOverhangAt(view.measure, Object.fromEntries(view.axes.map(a => [a, { mm: at[a] }])))\n return { touching: o.over, contact: o.contact, sides: o.sides as Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> }\n}\n\n/** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */\nexport const turnsInPlace = (asset: FigureAsset, part: string) =>\n (asset.joints ?? []).some(j => j.body1.part === part && ((j.motion ?? []).some(a => a.type === 'revolute') || j.origin?.facing === 'centre'))\n\n/**\n * How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up\n * for Y, which rises, so a front face is seen as it stands and not upside down.\n */\nexport function screenOf(axes: readonly [Axis, Axis]): { h: Axis; v: Axis; sign: Record<Axis, 1 | -1> } {\n const h = axes.includes('x') ? 'x' : 'z'\n const v = axes.find(a => a !== h)!\n return { h, v, sign: { x: 1, y: 1, z: 1, [v]: v === 'y' ? -1 : 1 } as Record<Axis, 1 | -1> }\n}\n\n/**\n * Where the fastening drawing writes its two names so neither lies over the other (user 2026-10-16: 「roller」 over the\n * roller's thin bar, and 「frame 윗면 2000 × 600」 cut by it). The face's name stands above the face, inside its top edge\n * only when the face fills the drawing. The part's name stands in the part when the part is wide and tall enough for it,\n * else just under it -- above it when the drawing ends first. Under a part as tall as the face, just under it is the\n * face's own edge, and the name lay on that line (2026-10-18, the roller in the fastening window): it goes under the edge.\n * Widths are estimated: 7 px a letter at 11-12 px; a line of it stands 11 px over its baseline and 3 under.\n */\nexport function fastenLabelsOf(\n face: { x: number; y: number; w: number; h: number },\n part: { x: number; y: number; w: number; h: number },\n partName: string,\n height: number\n): { face: { x: number; y: number }; part: { x: number; y: number; inside: boolean } } {\n const facePlace = face.y >= 16 ? { x: face.x, y: face.y - 5 } : { x: face.x + 4, y: face.y + 14 }\n const fits = part.w >= partName.length * 7 + 8 && part.h >= 16\n if (fits) return { face: facePlace, part: { x: part.x + part.w / 2, y: part.y + part.h / 2 + 4, inside: true } }\n const below = part.y + part.h + 14\n const edge = face.y + face.h\n if (below - 11 < edge + 2 && below + 3 > edge - 2 && edge + 14 <= height - 4) return { face: facePlace, part: { x: part.x + part.w / 2, y: edge + 14, inside: false } }\n return { face: facePlace, part: { x: part.x + part.w / 2, y: below <= height - 20 ? below : part.y - 5, inside: false } }\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import '@material/web/icon/icon.js';
|
|
|
2
2
|
import { LitElement, nothing } from 'lit';
|
|
3
3
|
import '@operato/input/ox-input-select-buttons.js';
|
|
4
4
|
import './figure-slot-layout-panel.js';
|
|
5
|
+
import './figure-size-editor.js';
|
|
5
6
|
import type { FigureRevision } from './revision.js';
|
|
6
7
|
declare const FigureLinks_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
7
8
|
/**
|
|
@@ -64,6 +65,8 @@ export declare class FigureLinks extends FigureLinks_base {
|
|
|
64
65
|
private row;
|
|
65
66
|
/** The motion form is open over the part's own motion, to change it where it is. */
|
|
66
67
|
private editingMotion;
|
|
68
|
+
/** Which of the joint's axes the form is over (ADR-0159); one past the last is a new axis. */
|
|
69
|
+
private motionAxis;
|
|
67
70
|
private loadedFor?;
|
|
68
71
|
willUpdate(changed: Map<string, unknown>): void;
|
|
69
72
|
updated(): void;
|
|
@@ -73,7 +76,6 @@ export declare class FigureLinks extends FigureLinks_base {
|
|
|
73
76
|
private renderPlaces;
|
|
74
77
|
/** The figure holds things: `Capacity` and `Holdable`, the ladder's middle rung. One part edit, one undo step. */
|
|
75
78
|
private declareHolding;
|
|
76
|
-
private renderDimensions;
|
|
77
79
|
/**
|
|
78
80
|
* 정렬·간격 고치기: the part moved inside the face it is fastened on, as one step -- the least move on each side it is
|
|
79
81
|
* out, or centred when it is wider than the face. When nothing of it can be moved, why is said under the button, not
|
|
@@ -88,9 +90,6 @@ export declare class FigureLinks extends FigureLinks_base {
|
|
|
88
90
|
* or take it off. Right after "follow size, attached" this is the result, not a refusal (designer 2026-09-27).
|
|
89
91
|
*/
|
|
90
92
|
private heldWords;
|
|
91
|
-
/** The same source, the same share, a new difference: a leaf the opening less its gaps (link-dimension's `plus`). */
|
|
92
|
-
private followPlus;
|
|
93
|
-
private follow;
|
|
94
93
|
/**
|
|
95
94
|
* Positions worked out from other values, each with the way to
|
|
96
95
|
* keep it where it is. This is the way out the release gate names when a resize leaves a part in the air.
|
|
@@ -3,10 +3,11 @@ import '@material/web/icon/icon.js';
|
|
|
3
3
|
import { css, html, LitElement, nothing } from 'lit';
|
|
4
4
|
import '@operato/input/ox-input-select-buttons.js';
|
|
5
5
|
import './figure-slot-layout-panel.js';
|
|
6
|
+
import './figure-size-editor.js';
|
|
6
7
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
7
8
|
import { i18next, localize } from '@operato/i18n';
|
|
8
|
-
import { figureFaceOverhangOf } from '@hatiolab/figure-model';
|
|
9
|
-
import { acceptsOf, AXES, canFasten, canFollow,
|
|
9
|
+
import { figureFaceOverhangOf, figureMotionsOf, motionIdOf } from '@hatiolab/figure-model';
|
|
10
|
+
import { acceptsOf, AXES, canFasten, canFollow, positionIsShare, heldBy, shownWhenOf, visibilityControlsOf, fasteningOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionFormOf, motionFormRefusalOf, endPlaceKeyOf, motionJoint, controlIdFor, withoutAxis, placeFacesOf, placeIdFor, placesOn, rowAxesOf, rowBlockOf, fixedJointOf, rowOf, positionsWorkedOut } from './links.js';
|
|
10
11
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js';
|
|
11
12
|
import { insideAlignmentOf } from './fasten-geometry.js';
|
|
12
13
|
import { fastenFormOf, fastenPlanOf, fastenSentenceOf } from './fasten-form.js';
|
|
@@ -65,6 +66,8 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
65
66
|
this.row = freshRow();
|
|
66
67
|
/** The motion form is open over the part's own motion, to change it where it is. */
|
|
67
68
|
this.editingMotion = false;
|
|
69
|
+
/** Which of the joint's axes the form is over (ADR-0159); one past the last is a new axis. */
|
|
70
|
+
this.motionAxis = 0;
|
|
68
71
|
}
|
|
69
72
|
static { this.styles = [SidePanelStyles, GroupMenuStyles, css `
|
|
70
73
|
:host {
|
|
@@ -104,8 +107,8 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
104
107
|
font-weight: 600;
|
|
105
108
|
}
|
|
106
109
|
/* What each direction does now: read-only rows. */
|
|
107
|
-
div[resize-now] { display: grid; gap: 4px; margin: 12px 0; }
|
|
108
|
-
div[resize-now] div[row] { grid-template-columns:
|
|
110
|
+
div[resize-now] { display: grid; grid-template-columns: 1fr auto auto auto; gap: 4px 8px; margin: 12px 0; }
|
|
111
|
+
div[resize-now] div[row] { grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; }
|
|
109
112
|
div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
|
|
110
113
|
/* Size behaviour is a choice about dimensions only. It never implies a placement or a fastening. */
|
|
111
114
|
/* One of the size group's rows: the group's gap spaces it, as it does the rest (a margin on top of it left a hole). */
|
|
@@ -115,13 +118,11 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
115
118
|
details { margin-top: 8px; }
|
|
116
119
|
details[row-fold] { margin: 0; }
|
|
117
120
|
details[row-fold] > summary { padding: 0; color: var(--md-sys-color-on-surface); }
|
|
118
|
-
details[row-fold] > summary h3 { margin: 0; }
|
|
121
|
+
details[row-fold] > summary h3 { margin: 0; flex: 1; }
|
|
119
122
|
details[row-fold][open] > summary { margin-bottom: 8px; }
|
|
120
123
|
/* What the command would do, said beside the result drawn on the canvas. */
|
|
121
124
|
ul[preview-words] { margin: 8px 0; padding: 8px 12px 8px 26px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); display: grid; gap: 4px; }
|
|
122
125
|
p[preview-words] { margin: 8px 0; padding: 8px 12px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); }
|
|
123
|
-
div[row-basis] { display: grid; gap: 4px; margin: 8px 0; }
|
|
124
|
-
div[row-basis] label { display: flex; align-items: center; gap: 6px; }
|
|
125
126
|
div[overhang] { display: grid; gap: 6px; margin: 8px 0; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--md-sys-color-outline-variant, #8884); }
|
|
126
127
|
div[overhang] p { margin: 0; }
|
|
127
128
|
ul[detach-words] { margin: 4px 0 10px; padding-left: 20px; display: grid; gap: 2px; }
|
|
@@ -151,7 +152,10 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
151
152
|
this.motion = freshMotion();
|
|
152
153
|
this.place = freshPlace();
|
|
153
154
|
this.row = freshRow();
|
|
155
|
+
this.rowOpen = false;
|
|
156
|
+
this.rowCandidate = undefined;
|
|
154
157
|
this.editingMotion = false;
|
|
158
|
+
this.motionAxis = 0;
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
updated() {
|
|
@@ -216,13 +220,14 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
216
220
|
How the part answers a size it is placed at (user 2026-10-17, the 「반응형」 tab): its own measures, where it
|
|
217
221
|
stands, and the row it may make -- what Build Once, Fit Anywhere is authored with, in one place.
|
|
218
222
|
*/
|
|
219
|
-
const positions = positionsWorkedOut(asset, this.partId);
|
|
223
|
+
const positions = held ? [] : positionsWorkedOut(asset, this.partId);
|
|
224
|
+
this.rowCandidate = undefined;
|
|
220
225
|
const response = html `
|
|
221
|
-
|
|
226
|
+
${canFollow(asset, this.partId) ? html `<section size>
|
|
222
227
|
<h3>${i18next.t('figure.label.group-size')}</h3>
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
</section
|
|
228
|
+
<figure-size-editor .revision=${this.revision} .partId=${this.partId} ?locked=${this.locked}></figure-size-editor>
|
|
229
|
+
<slot name="size-note"></slot>
|
|
230
|
+
</section>` : nothing}
|
|
226
231
|
${positions.length
|
|
227
232
|
? html `<section position-response>
|
|
228
233
|
<h3>${i18next.t('figure.label.group-position-response')}</h3>
|
|
@@ -235,10 +240,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
235
240
|
return response;
|
|
236
241
|
}
|
|
237
242
|
const shown = html `
|
|
238
|
-
<
|
|
239
|
-
<h3>${i18next.t('figure.label.group-size')}</h3>
|
|
240
|
-
<slot name="size"></slot>
|
|
241
|
-
</section>
|
|
243
|
+
<slot name="size"></slot>
|
|
242
244
|
<section fastening>
|
|
243
245
|
<h3>
|
|
244
246
|
${i18next.t('figure.label.group-place')}${this.help.button('figure.text.fastening-explained')}
|
|
@@ -316,73 +318,6 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
316
318
|
const names = [...new Set([...(asset.document.capabilities.names ?? []), 'Capacity', 'Holdable'])];
|
|
317
319
|
this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit: { kind: 'set-capabilities', names } }, bubbles: true, composed: true }));
|
|
318
320
|
}
|
|
319
|
-
renderDimensions(asset) {
|
|
320
|
-
const editable = followableAxes(asset, this.partId);
|
|
321
|
-
const round = editable.length === 2;
|
|
322
|
-
const word = (axis) => i18next.t(round && axis === 'x' ? 'figure.label.resize-diameter' : `figure.label.resize-${axis}`);
|
|
323
|
-
const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one));
|
|
324
|
-
const followsAll = editable.every(axis => followOf(asset, this.partId, axis).of === 'instance');
|
|
325
|
-
/* A size of its own on every axis is the usual: nothing to read, only the way to make it follow (user 2026-10-01). */
|
|
326
|
-
const allOwn = editable.every(axis => followOf(asset, this.partId, axis).of === 'own');
|
|
327
|
-
return html `${asset.sizing === 'uniform' ? html `<p hint>${i18next.t('figure.text.resize-uniform-warning')}</p>` : nothing}
|
|
328
|
-
${allOwn ? nothing : html `<div resize-now>${editable.map(axis => {
|
|
329
|
-
const now = followOf(asset, this.partId, axis);
|
|
330
|
-
const following = now.of === 'instance' && now.axis === axis;
|
|
331
|
-
const custom = now.of !== 'own' && !following;
|
|
332
|
-
const mm = measureNow(this.revision, this.partId, axis);
|
|
333
|
-
return html `<div row>
|
|
334
|
-
<b>${word(axis)}</b>
|
|
335
|
-
<span measure>${mm === undefined ? '—' : `${Math.round(mm)} mm`}</span>
|
|
336
|
-
<span>${custom ? followWordsOf(now, (key, params) => i18next.t(key, params)) : i18next.t(following ? 'figure.label.resize-following' : 'figure.label.follows-nothing')}</span>
|
|
337
|
-
${following
|
|
338
|
-
? html `<button quiet resize-unfollow=${axis} ?disabled=${this.locked}
|
|
339
|
-
@click=${() => {
|
|
340
|
-
this.follow(axis, 'own', now);
|
|
341
|
-
}}>${i18next.t('figure.button.resize-unfollow')}</button>`
|
|
342
|
-
: nothing}
|
|
343
|
-
</div>`;
|
|
344
|
-
})}</div>`}
|
|
345
|
-
${followsAll
|
|
346
|
-
? nothing
|
|
347
|
-
: html `<button act follow-size ?disabled=${this.locked || asset.sizing === 'uniform'} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
348
|
-
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
349
|
-
</button>`}
|
|
350
|
-
<details resize-choice><summary>${i18next.t('figure.label.resize-advanced')}</summary><div>${followableAxes(asset, this.partId).map(axis => {
|
|
351
|
-
const now = followOf(asset, this.partId, axis);
|
|
352
|
-
const value = choiceOf(now);
|
|
353
|
-
/* A cylinder's two measures are its radius and its length; its X and Y name them. */
|
|
354
|
-
const name = round ? i18next.t(axis === 'x' ? 'figure.label.field-radius' : 'figure.label.field-length') : axis.toUpperCase();
|
|
355
|
-
return html `<div row>
|
|
356
|
-
<b>${name}</b>
|
|
357
|
-
<select ?disabled=${this.locked} @change=${(e) => this.follow(axis, e.target.value, now)}>
|
|
358
|
-
<option value="own" ?selected=${value === 'own'}>${i18next.t('figure.label.follows-nothing')}</option>
|
|
359
|
-
${AXES.filter(one => hasInstanceSize(asset, one)).map(one => html `<option value=${`share:${one}`} ?selected=${value === `share:${one}`}>
|
|
360
|
-
${i18next.t('figure.label.follows-instance-share', { axis: one.toUpperCase() })}
|
|
361
|
-
</option>
|
|
362
|
-
<option value=${`instance:${one}`} ?selected=${value === `instance:${one}`}>
|
|
363
|
-
${i18next.t('figure.label.follows-instance', { axis: one.toUpperCase() })}
|
|
364
|
-
</option>`)}
|
|
365
|
-
${others.flatMap(other => AXES.map(one => html `<option value=${`part:${other}:${one}`} ?selected=${value === `part:${other}:${one}`}>${i18next.t('figure.label.follows-part-axis', { part: other, axis: one.toUpperCase() })}</option>`))}
|
|
366
|
-
${now.of === 'expression' || now.of === 'input'
|
|
367
|
-
? html `<option value=${value} selected>${now.of === 'input' ? now.input : i18next.t('figure.label.follows-expression')}</option>`
|
|
368
|
-
: nothing}
|
|
369
|
-
</select>
|
|
370
|
-
${now.of === 'instance' || now.of === 'part'
|
|
371
|
-
? html `<span></span>
|
|
372
|
-
<label inline-plus>
|
|
373
|
-
${i18next.t('figure.label.follows-plus')}
|
|
374
|
-
<input
|
|
375
|
-
type="number"
|
|
376
|
-
step="any"
|
|
377
|
-
?disabled=${this.locked}
|
|
378
|
-
.value=${String(now.plus ?? 0)}
|
|
379
|
-
@change=${(e) => this.followPlus(axis, now, Number(e.target.value) || 0)}
|
|
380
|
-
/>
|
|
381
|
-
</label>`
|
|
382
|
-
: nothing}
|
|
383
|
-
</div>`;
|
|
384
|
-
})}</div></details>`;
|
|
385
|
-
}
|
|
386
321
|
/**
|
|
387
322
|
* 정렬·간격 고치기: the part moved inside the face it is fastened on, as one step -- the least move on each side it is
|
|
388
323
|
* out, or centred when it is wider than the face. When nothing of it can be moved, why is said under the button, not
|
|
@@ -431,21 +366,6 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
431
366
|
: i18next.t('figure.text.held-on-part', { other: joint.body0.part, otherFace: i18next.t(endPlaceKeyOf(joint.body0)) });
|
|
432
367
|
return where;
|
|
433
368
|
}
|
|
434
|
-
/** The same source, the same share, a new difference: a leaf the opening less its gaps (link-dimension's `plus`). */
|
|
435
|
-
followPlus(axis, now, plus) {
|
|
436
|
-
if (now.of !== 'instance' && now.of !== 'part')
|
|
437
|
-
return;
|
|
438
|
-
if ((now.plus ?? 0) === plus)
|
|
439
|
-
return;
|
|
440
|
-
const source = now.of === 'instance' ? { of: 'instance', axis: now.axis } : { of: 'part', part: now.part, axis: now.axis };
|
|
441
|
-
const times = now.of === 'instance' ? now.times : (now.times ?? 1);
|
|
442
|
-
this.command({ kind: 'link-dimension', part: this.partId, axis, source, ...(times !== 1 ? { times } : {}), ...(plus ? { plus } : {}), replace: true });
|
|
443
|
-
}
|
|
444
|
-
follow(axis, choice, now) {
|
|
445
|
-
const action = followCommandOf(this.revision, this.partId, axis, choice, now);
|
|
446
|
-
if (action)
|
|
447
|
-
this.command(action);
|
|
448
|
-
}
|
|
449
369
|
/**
|
|
450
370
|
* Positions worked out from other values, each with the way to
|
|
451
371
|
* keep it where it is. This is the way out the release gate names when a resize leaves a part in the air.
|
|
@@ -544,9 +464,9 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
544
464
|
Folded, open while it is being worked (2026-10-02): a row is made now and then, and most parts stand on the plane,
|
|
545
465
|
where the section said its reason open on every one. Opened, it says why or asks how.
|
|
546
466
|
*/
|
|
547
|
-
const head = html `<summary><h3>${i18next.t('figure.label.repeat')}</h3></summary>`;
|
|
467
|
+
const head = html `<summary><h3>${i18next.t('figure.label.repeat')} <span section-state>${i18next.t('figure.label.repeat-inactive')}</span></h3></summary>`;
|
|
548
468
|
if (blocked || !axes.length)
|
|
549
|
-
return html `<section row><details row-fold>
|
|
469
|
+
return html `<section row><details row-fold open>
|
|
550
470
|
${head}
|
|
551
471
|
<p hint row-blocked>${i18next.t(`figure.text.row-blocked-${blocked ?? 'unfastened'}`, { part: this.partId })}</p>
|
|
552
472
|
${blocked === 'unfastened'
|
|
@@ -567,27 +487,29 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
567
487
|
const toggled = (e) => (this.rowOpen = e.target.open);
|
|
568
488
|
return html `<section row><details row-fold ?open=${this.rowOpen} @toggle=${toggled}>
|
|
569
489
|
${head}
|
|
490
|
+
<p hint>${i18next.t('figure.text.repeat-intro')}</p>
|
|
570
491
|
<div form>
|
|
571
492
|
<span>${i18next.t('figure.label.repeat-axis')}</span>
|
|
572
|
-
<select @change=${(e) => set({ ...r, axis: e.target.value })}>
|
|
493
|
+
<select aria-label=${i18next.t('figure.label.repeat-axis')} ?disabled=${this.locked} @change=${(e) => set({ ...r, axis: e.target.value })}>
|
|
573
494
|
${axes.map(a => html `<option value=${a} ?selected=${a === axis}>${a.toUpperCase()}</option>`)}
|
|
574
495
|
</select>
|
|
575
496
|
</div>
|
|
576
|
-
<
|
|
577
|
-
|
|
578
|
-
<label><input type="radio" name="row-basis" .checked=${
|
|
497
|
+
<p hint>${i18next.t('figure.text.repeat-direction')}</p>
|
|
498
|
+
<div choice-row role="radiogroup" aria-label=${i18next.t('figure.label.row-basis')}>
|
|
499
|
+
<label toggle><input type="radio" name="row-basis" ?disabled=${this.locked} .checked=${r.fills} @change=${() => set({ ...r, fills: true })} />${i18next.t('figure.label.repeat-fills')}</label>
|
|
500
|
+
<label toggle><input type="radio" name="row-basis" ?disabled=${this.locked} .checked=${!r.fills} @change=${() => set({ ...r, fills: false })} />${i18next.t('figure.label.row-by-count')}</label>
|
|
579
501
|
</div>
|
|
580
502
|
${r.fills
|
|
581
503
|
? html `<div form>
|
|
582
504
|
<span>${i18next.t('figure.label.repeat-pitch')} · mm</span>
|
|
583
|
-
<input type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e) => set({ ...r, pitch: Number(e.target.value) })} />
|
|
505
|
+
<input aria-label=${i18next.t('figure.label.repeat-pitch')} ?disabled=${this.locked} type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e) => set({ ...r, pitch: Number(e.target.value) })} />
|
|
584
506
|
</div>`
|
|
585
507
|
: html `<div form>
|
|
586
508
|
<span>${i18next.t('figure.label.repeat-count')}</span>
|
|
587
|
-
<input type="number" step="1" min="1" .value=${String(r.count)} @change=${(e) => set({ ...r, count: Number(e.target.value) })} />
|
|
509
|
+
<input aria-label=${i18next.t('figure.label.repeat-count')} ?disabled=${this.locked} type="number" step="1" min="1" .value=${String(r.count)} @change=${(e) => set({ ...r, count: Number(e.target.value) })} />
|
|
588
510
|
</div>
|
|
589
511
|
<p hint>${i18next.t('figure.text.row-count-spreads')}</p>`}
|
|
590
|
-
${action && this.rowOpen ? this.renderPreviewWords() : nothing}
|
|
512
|
+
${action && this.rowOpen ? html `<p hint>${i18next.t('figure.text.repeat-draft')}</p>${this.renderPreviewWords()}` : nothing}
|
|
591
513
|
<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>
|
|
592
514
|
<md-icon>view_week</md-icon>${i18next.t('figure.button.make-repeat')}
|
|
593
515
|
</button>
|
|
@@ -643,6 +565,45 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
643
565
|
/* Which part moves, against what, which way, how far -- by name (ADR-0097 §5.5). */
|
|
644
566
|
const against = joint ? ('plane' in joint.body0 ? i18next.t('figure.label.mounting-plane') : joint.body0.part) : '';
|
|
645
567
|
const ends = this.motionEnds;
|
|
568
|
+
/* One row an axis (ADR-0159): how it moves and by what, changed or taken away where it is; more added under them. */
|
|
569
|
+
const count = joint ? figureMotionsOf(joint).length : 1;
|
|
570
|
+
const edit = (k) => {
|
|
571
|
+
if (!joint)
|
|
572
|
+
return;
|
|
573
|
+
this.motionAxis = k;
|
|
574
|
+
this.motion = motionFormOf(joint, k);
|
|
575
|
+
this.editingMotion = true;
|
|
576
|
+
};
|
|
577
|
+
const row = (k) => {
|
|
578
|
+
const it = k === 0 ? now : motionOf(asset, this.partId, k);
|
|
579
|
+
if (!it)
|
|
580
|
+
return nothing;
|
|
581
|
+
return html `<div pair motion-axis=${k}>
|
|
582
|
+
<span>
|
|
583
|
+
${count > 1 ? html `<b>${i18next.t('figure.label.motion-axis-n', { n: k + 1 })}</b> ` : nothing}${i18next.t('figure.label.motion-' + it.kind)} · ${it.vector ? jointAxisWord(it.vector) : String(it.axis).toUpperCase()}
|
|
584
|
+
${it.travel
|
|
585
|
+
? html `<small>${i18next.t('figure.label.motion-travel-is', { part: it.travel.part, axis: it.travel.axis.toUpperCase(), plus: it.travel.plus ?? 0 })}</small>`
|
|
586
|
+
: nothing}
|
|
587
|
+
${it.follows?.sum.length === 1
|
|
588
|
+
? html `<small>${i18next.t('figure.label.motion-follows', { joint: it.follows.sum[0].joint, multiplier: it.follows.sum[0].multiplier, offset: it.follows.offset })}</small>`
|
|
589
|
+
: it.follows
|
|
590
|
+
? html `<small>${i18next.t('figure.label.motion-follow')}: ${it.follows.sum.map(t => `${t.joint} × ${t.multiplier}`).join(' + ')} + ${it.follows.offset}</small>`
|
|
591
|
+
: it.control
|
|
592
|
+
? html `<small>${it.control.id} (${it.control.unit}) ${it.control.min} … ${it.control.max}</small>`
|
|
593
|
+
: nothing}
|
|
594
|
+
</span>
|
|
595
|
+
${count > 1
|
|
596
|
+
? html `<button act remove-axis ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: withoutAxis(joint, k) })}>
|
|
597
|
+
<md-icon>close</md-icon>${i18next.t('figure.button.remove-axis')}
|
|
598
|
+
</button>`
|
|
599
|
+
: html `<button act ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}>
|
|
600
|
+
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
601
|
+
</button>`}
|
|
602
|
+
<button act ?disabled=${this.locked || !joint} @click=${() => edit(k)}>
|
|
603
|
+
<md-icon>edit</md-icon>${i18next.t('figure.button.change-motion')}
|
|
604
|
+
</button>
|
|
605
|
+
</div>`;
|
|
606
|
+
};
|
|
646
607
|
return html `<p motion-summary>
|
|
647
608
|
${i18next.t('figure.text.motion-summary', {
|
|
648
609
|
part: this.partId,
|
|
@@ -654,40 +615,17 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
654
615
|
${ends
|
|
655
616
|
? html `<p hint motion-ends>${i18next.t('figure.text.motion-ends-shown', { min: String(ends.min), max: String(ends.max), unit: ends.unit })}</p>`
|
|
656
617
|
: nothing}
|
|
618
|
+
${Array.from({ length: count }, (_, k) => row(k))}
|
|
657
619
|
<div pair>
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
620
|
+
<button act add-axis ?disabled=${this.locked || !joint} @click=${() => edit(count)}>
|
|
621
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-axis')}
|
|
622
|
+
</button>
|
|
623
|
+
${count > 1
|
|
624
|
+
? html `<button act ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}>
|
|
625
|
+
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
626
|
+
</button>`
|
|
662
627
|
: nothing}
|
|
663
|
-
|
|
664
|
-
? html `<small>${i18next.t('figure.label.motion-follows', { joint: now.follows.sum[0].joint, multiplier: now.follows.sum[0].multiplier, offset: now.follows.offset })}</small>`
|
|
665
|
-
: now.follows
|
|
666
|
-
? html `<small>${i18next.t('figure.label.motion-follow')}: ${now.follows.sum.map(t => `${t.joint} × ${t.multiplier}`).join(' + ')} + ${now.follows.offset}</small>`
|
|
667
|
-
: now.control
|
|
668
|
-
? html `<small>${now.control.id} (${now.control.unit}) ${now.control.min} … ${now.control.max}</small>`
|
|
669
|
-
: nothing}
|
|
670
|
-
</span>
|
|
671
|
-
<button
|
|
672
|
-
act
|
|
673
|
-
?disabled=${this.locked || !joint}
|
|
674
|
-
@click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}
|
|
675
|
-
>
|
|
676
|
-
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
677
|
-
</button>
|
|
678
|
-
<button
|
|
679
|
-
act
|
|
680
|
-
?disabled=${this.locked || !joint}
|
|
681
|
-
@click=${() => {
|
|
682
|
-
if (!joint)
|
|
683
|
-
return;
|
|
684
|
-
this.motion = motionFormOf(joint);
|
|
685
|
-
this.editingMotion = true;
|
|
686
|
-
}}
|
|
687
|
-
>
|
|
688
|
-
<md-icon>edit</md-icon>${i18next.t('figure.button.change-motion')}
|
|
689
|
-
</button>
|
|
690
|
-
</div>`;
|
|
628
|
+
</div>`;
|
|
691
629
|
}
|
|
692
630
|
/* A joint needs a parent (URDF): a part fastened to nothing is fastened first. */
|
|
693
631
|
if (!joint)
|
|
@@ -698,10 +636,13 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
698
636
|
const m = this.motion;
|
|
699
637
|
const unit = m.kind === 'slide' ? 'mm' : 'deg';
|
|
700
638
|
/* A follower follows a joint that moves the same way; a choice the kind no longer allows is dropped. */
|
|
701
|
-
const
|
|
639
|
+
const k = this.motionAxis;
|
|
640
|
+
const leaders = leadersFor(asset, this.partId, m.kind, motionIdOf(joint, k));
|
|
641
|
+
const adding = k > 0 && k >= figureMotionsOf(joint).length;
|
|
702
642
|
const follow = leaders.some(j => j.id === m.follow) ? m.follow : '';
|
|
703
643
|
const refusal = motionFormRefusalOf(m, follow);
|
|
704
644
|
return html `
|
|
645
|
+
${k > 0 ? html `<p motion-axis-heading><b>${i18next.t('figure.label.motion-axis-n', { n: k + 1 })}</b></p>` : nothing}
|
|
705
646
|
<div pair>
|
|
706
647
|
<select @change=${(e) => this.setKind(e.target.value)}>
|
|
707
648
|
<option value="slide" ?selected=${m.kind === 'slide'}>${i18next.t('figure.label.motion-slide')}</option>
|
|
@@ -726,7 +667,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
726
667
|
<span>${i18next.t('figure.label.motion-follow')}</span>
|
|
727
668
|
<select @change=${(e) => (this.motion = { ...m, follow: e.target.value })}>
|
|
728
669
|
<option value="" ?selected=${!follow}>${i18next.t('figure.label.motion-own-control')}</option>
|
|
729
|
-
${leaders.map(j => html `<option value=${j.id} ?selected=${j.id === follow}>${j.
|
|
670
|
+
${leaders.map(j => html `<option value=${j.id} ?selected=${j.id === follow}>${j.part}${j.state?.id ? ` · ${j.state.id}` : j.id !== j.part ? ` · ${j.id}` : ''}</option>`)}
|
|
730
671
|
</select>
|
|
731
672
|
</div>`
|
|
732
673
|
: nothing}
|
|
@@ -757,7 +698,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
757
698
|
: nothing}`
|
|
758
699
|
: nothing}
|
|
759
700
|
<span>${i18next.t('figure.label.control-id')}</span>
|
|
760
|
-
<input .value=${m.id} placeholder=${
|
|
701
|
+
<input .value=${m.id} placeholder=${controlIdFor(this.partId, m.kind, k)} @change=${(e) => (this.motion = { ...m, id: e.target.value.trim() })} />
|
|
761
702
|
${m.kind === 'continuous' || (m.kind === 'slide' && (m.travel || m.travelSum))
|
|
762
703
|
? nothing
|
|
763
704
|
: html `<span>${i18next.t('figure.label.control-range')} (${unit})</span>
|
|
@@ -771,14 +712,15 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
771
712
|
?disabled=${this.locked || !!refusal}
|
|
772
713
|
title=${refusal === 'range' ? i18next.t('figure.text.range-needs-room') : ''}
|
|
773
714
|
@click=${() => {
|
|
774
|
-
this.command({ kind: 'set-joint', joint: motionJoint(joint, { ...m, follow }, this.partId) });
|
|
715
|
+
this.command({ kind: 'set-joint', joint: motionJoint(joint, { ...m, follow }, this.partId, k) });
|
|
775
716
|
this.editingMotion = false;
|
|
717
|
+
this.motionAxis = 0;
|
|
776
718
|
}}
|
|
777
719
|
>
|
|
778
|
-
<md-icon>animation</md-icon>${i18next.t(this.editingMotion ? 'figure.button.apply-motion-change' : 'figure.button.add-motion')}
|
|
720
|
+
<md-icon>animation</md-icon>${i18next.t(adding ? 'figure.button.add-axis' : this.editingMotion ? 'figure.button.apply-motion-change' : 'figure.button.add-motion')}
|
|
779
721
|
</button>
|
|
780
722
|
${this.editingMotion
|
|
781
|
-
? html `<button act @click=${() => (this.editingMotion = false)}><md-icon>undo</md-icon>${i18next.t('figure.button.cancel-motion-change')}</button>`
|
|
723
|
+
? html `<button act @click=${() => ((this.editingMotion = false), (this.motionAxis = 0))}><md-icon>undo</md-icon>${i18next.t('figure.button.cancel-motion-change')}</button>`
|
|
782
724
|
: nothing}
|
|
783
725
|
`;
|
|
784
726
|
}
|
|
@@ -857,6 +799,10 @@ __decorate([
|
|
|
857
799
|
state(),
|
|
858
800
|
__metadata("design:type", Object)
|
|
859
801
|
], FigureLinks.prototype, "editingMotion", void 0);
|
|
802
|
+
__decorate([
|
|
803
|
+
state(),
|
|
804
|
+
__metadata("design:type", Object)
|
|
805
|
+
], FigureLinks.prototype, "motionAxis", void 0);
|
|
860
806
|
FigureLinks = __decorate([
|
|
861
807
|
customElement('figure-links')
|
|
862
808
|
], FigureLinks);
|