@things-factory/figure-ui 10.1.150 → 10.1.152

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.
Files changed (44) hide show
  1. package/client/modeller/editor.ts +15 -15
  2. package/client/modeller/fasten-geometry.ts +32 -0
  3. package/client/modeller/figure-links.ts +32 -14
  4. package/client/modeller/mesh-import.ts +0 -10
  5. package/client/modeller/refusals.ts +14 -1
  6. package/dist-client/modeller/editor.js +16 -16
  7. package/dist-client/modeller/editor.js.map +1 -1
  8. package/dist-client/modeller/fasten-geometry.d.ts +13 -0
  9. package/dist-client/modeller/fasten-geometry.js +29 -0
  10. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  11. package/dist-client/modeller/figure-links.d.ts +9 -2
  12. package/dist-client/modeller/figure-links.js +35 -14
  13. package/dist-client/modeller/figure-links.js.map +1 -1
  14. package/dist-client/modeller/mesh-import.d.ts +0 -2
  15. package/dist-client/modeller/mesh-import.js +0 -11
  16. package/dist-client/modeller/mesh-import.js.map +1 -1
  17. package/dist-client/modeller/product-session.d.ts +4 -28
  18. package/dist-client/modeller/refusals.d.ts +2 -1
  19. package/dist-client/modeller/refusals.js +17 -1
  20. package/dist-client/modeller/refusals.js.map +1 -1
  21. package/dist-client/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +6 -6
  23. package/test/browser/build.mjs +7 -2
  24. package/test/browser/editing.check.mjs +14 -0
  25. package/test/browser/fix-alignment.check.mjs +101 -0
  26. package/test/browser/links.check.mjs +34 -5
  27. package/test/browser/mesh-select.check.mjs +91 -0
  28. package/test/browser/modeller-page.js +36 -0
  29. package/test/browser/modeller-revision.check.mjs +18 -6
  30. package/test/browser/part-ids.check.mjs +104 -0
  31. package/test/browser/parts.check.mjs +39 -2
  32. package/test/browser/row-preview.check.mjs +81 -0
  33. package/test/browser/screen.check.mjs +2 -1
  34. package/test/fields.test.ts +5 -9
  35. package/test/fix-alignment.test.ts +32 -0
  36. package/test/mesh-import.test.ts +1 -8
  37. package/test/parts-wiring.test.ts +1 -5
  38. package/test/refusals.test.ts +28 -0
  39. package/translations/en.json +8 -0
  40. package/translations/es.json +8 -0
  41. package/translations/ja.json +8 -0
  42. package/translations/ko.json +8 -0
  43. package/translations/ms.json +8 -0
  44. package/translations/zh.json +8 -0
@@ -38,6 +38,19 @@ export declare function alignmentAt(d: number, face: number | undefined, part: n
38
38
  mm?: number;
39
39
  caught?: 'centre' | 'inside' | 'outside';
40
40
  };
41
+ /**
42
+ * The alignment that brings the footprint inside the face, for 정렬·간격 고치기: on each axis it comes past an edge, the
43
+ * least move -- to that edge from inside (`min` / `max`, or that distance where edges are not offered) -- or the centre
44
+ * when the part is wider than the face and cannot be inside. `inside` when nothing of it is past an edge: what is out
45
+ * is something fastened on it. `unmeasured` when the face has no size to measure against.
46
+ */
47
+ export declare function insideAlignmentOf(view: FaceView | undefined, align: Partial<Record<Axis, Alignment>>, alignMm: Partial<Record<Axis, number>>): {
48
+ align: Partial<Record<Axis, Alignment>>;
49
+ alignMm: Partial<Record<Axis, number>>;
50
+ moved: Axis[];
51
+ } | {
52
+ cannot: 'inside' | 'unmeasured';
53
+ };
41
54
  /** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */
42
55
  export declare function overhangOf(view: FaceView, at: Record<Axis, number>): {
43
56
  touching: boolean;
@@ -108,6 +108,35 @@ export function alignmentAt(d, face, part, edges, catchMm) {
108
108
  }
109
109
  return { how: 'mm', mm: Math.round(d) };
110
110
  }
111
+ /**
112
+ * The alignment that brings the footprint inside the face, for 정렬·간격 고치기: on each axis it comes past an edge, the
113
+ * least move -- to that edge from inside (`min` / `max`, or that distance where edges are not offered) -- or the centre
114
+ * when the part is wider than the face and cannot be inside. `inside` when nothing of it is past an edge: what is out
115
+ * is something fastened on it. `unmeasured` when the face has no size to measure against.
116
+ */
117
+ export function insideAlignmentOf(view, align, alignMm) {
118
+ if (!view || view.axes.some(a => view.face[a] === undefined))
119
+ return { cannot: 'unmeasured' };
120
+ const next = { align: { ...align }, alignMm: { ...alignMm }, moved: [] };
121
+ for (const a of view.axes) {
122
+ const face = view.face[a], part = view.part[a];
123
+ const at = offsetOf(align[a], alignMm[a], face, part);
124
+ const low = -face / 2 - (at - part / 2) > 0.5, high = at + part / 2 - face / 2 > 0.5;
125
+ if (!low && !high)
126
+ continue;
127
+ next.moved.push(a);
128
+ if (part > face) {
129
+ next.align[a] = 'centre';
130
+ continue;
131
+ }
132
+ const edge = ((low ? -1 : 1) * (face - part)) / 2;
133
+ if (view.edges[a])
134
+ next.align[a] = low ? 'min' : 'max';
135
+ else
136
+ ((next.align[a] = 'mm'), (next.alignMm[a] = Math.round(edge)));
137
+ }
138
+ return next.moved.length ? next : { cannot: 'inside' };
139
+ }
111
140
  /** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */
112
141
  export function overhangOf(view, at) {
113
142
  const sides = [];
@@ -1 +1 @@
1
- {"version":3,"file":"fasten-geometry.js","sourceRoot":"","sources":["../../client/modeller/fasten-geometry.ts"],"names":[],"mappings":"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","sourcesContent":["import { figurePartFaceOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureFace } from '@hatiolab/figure-model'\n\nimport { boxOf } from './align.js'\nimport { alignAxesOf, facesOf, faceAxisOf, hasInstanceSize, mountingPlaneOf, 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's\n * own face follows from those -- the kernel takes no other pairing (figure-model `attach`: meet is the facing face,\n * flush the same one, a face along another axis would need a turn).\n */\n\ntype Axis = 'x' | 'y' | 'z'\nconst AXES: Axis[] = ['x', 'y', 'z']\nexport type FastenMode = 'meet' | 'flush' | 'centre'\nexport type FastenTarget = { part: string; face: string } | 'plane'\n\nconst OPPOSITE: Record<FigureFace, FigureFace> = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' }\nconst NORMAL: Record<FigureFace, [Axis, 1 | -1]> = { left: ['x', -1], right: ['x', 1], bottom: ['y', -1], top: ['y', 1], back: ['z', -1], front: ['z', 1] }\n\n/** The world direction a part's face looks along now, as drawn, turns and all; by its name when the face's outline is not read. */\nfunction directionOf(revision: FigureRevision, part: string, face: string): [Axis, 1 | -1] | undefined {\n const area = (() => {\n try {\n return figurePartFaceOf(revision.asset, part, face, revision.inputs)\n } catch {\n return undefined\n }\n })()\n if (area) {\n const k = area.normal.findIndex(v => Math.abs(v) > 0.5)\n if (k >= 0) return [AXES[k]!, area.normal[k]! > 0 ? 1 : -1]\n }\n return face in NORMAL ? NORMAL[face as FigureFace] : undefined\n}\n\n/**\n * The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its\n * centre. Read from where each face looks now, so a part standing turned offers the face that is really there.\n * Undefined when the part has no such face.\n */\nexport function myFaceFor(revision: FigureRevision, part: string, target: FastenTarget, mode: FastenMode): FigureFace | 'centre' | undefined {\n const asset = revision.asset\n if (mode === 'centre') return target === 'plane' ? undefined : 'centre'\n const want: [Axis, 1 | -1] | undefined =\n target === 'plane'\n ? /* The plane is met from above on a floor, from in front on a wall. */ [mountingPlaneOf(asset).normal, -1]\n : (() => {\n const d = directionOf(revision, target.part, target.face)\n return d && [d[0], (mode === 'meet' ? -d[1] : d[1]) as 1 | -1]\n })()\n if (!want) return undefined\n const mine = facesOf(asset, part).filter((f): f is FigureFace => f in NORMAL)\n const hit = mine.find(f => {\n const d = directionOf(revision, part, f)\n return d && d[0] === want[0] && d[1] === want[1]\n })\n return hit ?? (target !== 'plane' && mode === 'meet' && target.face in OPPOSITE ? OPPOSITE[target.face as FigureFace] : undefined)\n}\n\n/** Which way the fastening is, read back from the part's face and facing. */\nexport const modeOf = (face: string, facing: 'meet' | 'flush'): FastenMode => (face === 'centre' ? 'centre' : facing)\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 -- none\n * for a plane with no declared size -- the part's footprint on each, and whether its edges can be lined up with.\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}\n\nexport function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget): FaceView | undefined {\n const asset = revision.asset\n const across = alignAxesOf(asset, part, target)\n if (across.length !== 2) return undefined\n const box = boxOf(revision, part)\n if (!box) return undefined\n const axes = [across[0]!.axis, across[1]!.axis] as [Axis, Axis]\n const face: Partial<Record<Axis, number>> = {}\n if (target === 'plane') {\n for (const a of axes) if (hasInstanceSize(asset, a)) face[a] = asset.designInputs[`size.${a}`]!\n } else {\n const theirs = boxOf(revision, target.part)\n const normal = faceAxisOf(asset, target.part, target.face)\n for (const a of axes) if (theirs && a !== normal) face[a] = theirs.extent[a]\n }\n return {\n axes,\n face,\n part: { x: box.extent.x, y: box.extent.y, z: box.extent.z },\n edges: Object.fromEntries(across.map(o => [o.axis, o.edges])) as Record<Axis, boolean>\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. */\nexport function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number {\n if (how === 'min' && face !== undefined) return -(face - part) / 2\n if (how === 'max' && face !== undefined) return (face - part) / 2\n if (how === 'mm' || how === 'share') return mm ?? 0\n return 0\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/** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */\nexport function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {\n const sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> = []\n let touching = true\n for (const a of view.axes) {\n const face = view.face[a]\n if (face === undefined) continue\n const lo = at[a] - view.part[a] / 2,\n hi = at[a] + view.part[a] / 2\n if (lo >= face / 2 || hi <= -face / 2) touching = false\n if (-face / 2 - lo > 0.5) sides.push({ axis: a, side: 'low', mm: Math.round(-face / 2 - lo) })\n if (hi - face / 2 > 0.5) sides.push({ axis: a, side: 'high', mm: Math.round(hi - face / 2) })\n }\n return { touching, sides }\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' || ('face' in j.body1 && j.body1.face === '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"]}
1
+ {"version":3,"file":"fasten-geometry.js","sourceRoot":"","sources":["../../client/modeller/fasten-geometry.ts"],"names":[],"mappings":"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","sourcesContent":["import { figurePartFaceOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureFace } from '@hatiolab/figure-model'\n\nimport { boxOf } from './align.js'\nimport { alignAxesOf, facesOf, faceAxisOf, hasInstanceSize, mountingPlaneOf, 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's\n * own face follows from those -- the kernel takes no other pairing (figure-model `attach`: meet is the facing face,\n * flush the same one, a face along another axis would need a turn).\n */\n\ntype Axis = 'x' | 'y' | 'z'\nconst AXES: Axis[] = ['x', 'y', 'z']\nexport type FastenMode = 'meet' | 'flush' | 'centre'\nexport type FastenTarget = { part: string; face: string } | 'plane'\n\nconst OPPOSITE: Record<FigureFace, FigureFace> = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' }\nconst NORMAL: Record<FigureFace, [Axis, 1 | -1]> = { left: ['x', -1], right: ['x', 1], bottom: ['y', -1], top: ['y', 1], back: ['z', -1], front: ['z', 1] }\n\n/** The world direction a part's face looks along now, as drawn, turns and all; by its name when the face's outline is not read. */\nfunction directionOf(revision: FigureRevision, part: string, face: string): [Axis, 1 | -1] | undefined {\n const area = (() => {\n try {\n return figurePartFaceOf(revision.asset, part, face, revision.inputs)\n } catch {\n return undefined\n }\n })()\n if (area) {\n const k = area.normal.findIndex(v => Math.abs(v) > 0.5)\n if (k >= 0) return [AXES[k]!, area.normal[k]! > 0 ? 1 : -1]\n }\n return face in NORMAL ? NORMAL[face as FigureFace] : undefined\n}\n\n/**\n * The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its\n * centre. Read from where each face looks now, so a part standing turned offers the face that is really there.\n * Undefined when the part has no such face.\n */\nexport function myFaceFor(revision: FigureRevision, part: string, target: FastenTarget, mode: FastenMode): FigureFace | 'centre' | undefined {\n const asset = revision.asset\n if (mode === 'centre') return target === 'plane' ? undefined : 'centre'\n const want: [Axis, 1 | -1] | undefined =\n target === 'plane'\n ? /* The plane is met from above on a floor, from in front on a wall. */ [mountingPlaneOf(asset).normal, -1]\n : (() => {\n const d = directionOf(revision, target.part, target.face)\n return d && [d[0], (mode === 'meet' ? -d[1] : d[1]) as 1 | -1]\n })()\n if (!want) return undefined\n const mine = facesOf(asset, part).filter((f): f is FigureFace => f in NORMAL)\n const hit = mine.find(f => {\n const d = directionOf(revision, part, f)\n return d && d[0] === want[0] && d[1] === want[1]\n })\n return hit ?? (target !== 'plane' && mode === 'meet' && target.face in OPPOSITE ? OPPOSITE[target.face as FigureFace] : undefined)\n}\n\n/** Which way the fastening is, read back from the part's face and facing. */\nexport const modeOf = (face: string, facing: 'meet' | 'flush'): FastenMode => (face === 'centre' ? 'centre' : facing)\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 -- none\n * for a plane with no declared size -- the part's footprint on each, and whether its edges can be lined up with.\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}\n\nexport function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget): FaceView | undefined {\n const asset = revision.asset\n const across = alignAxesOf(asset, part, target)\n if (across.length !== 2) return undefined\n const box = boxOf(revision, part)\n if (!box) return undefined\n const axes = [across[0]!.axis, across[1]!.axis] as [Axis, Axis]\n const face: Partial<Record<Axis, number>> = {}\n if (target === 'plane') {\n for (const a of axes) if (hasInstanceSize(asset, a)) face[a] = asset.designInputs[`size.${a}`]!\n } else {\n const theirs = boxOf(revision, target.part)\n const normal = faceAxisOf(asset, target.part, target.face)\n for (const a of axes) if (theirs && a !== normal) face[a] = theirs.extent[a]\n }\n return {\n axes,\n face,\n part: { x: box.extent.x, y: box.extent.y, z: box.extent.z },\n edges: Object.fromEntries(across.map(o => [o.axis, o.edges])) as Record<Axis, boolean>\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. */\nexport function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number {\n if (how === 'min' && face !== undefined) return -(face - part) / 2\n if (how === 'max' && face !== undefined) return (face - part) / 2\n if (how === 'mm' || how === 'share') return mm ?? 0\n return 0\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/**\n * The alignment that brings the footprint inside the face, for 정렬·간격 고치기: on each axis it comes past an edge, the\n * least move -- to that edge from inside (`min` / `max`, or that distance where edges are not offered) -- or the centre\n * when the part is wider than the face and cannot be inside. `inside` when nothing of it is past an edge: what is out\n * is something 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 || view.axes.some(a => view.face[a] === undefined)) return { cannot: 'unmeasured' }\n const next = { align: { ...align }, alignMm: { ...alignMm }, moved: [] as Axis[] }\n for (const a of view.axes) {\n const face = view.face[a]!,\n part = view.part[a]\n const at = offsetOf(align[a], alignMm[a], face, part)\n const low = -face / 2 - (at - part / 2) > 0.5,\n high = at + part / 2 - face / 2 > 0.5\n if (!low && !high) continue\n next.moved.push(a)\n if (part > face) {\n next.align[a] = 'centre'\n continue\n }\n const edge = ((low ? -1 : 1) * (face - part)) / 2\n if (view.edges[a]) next.align[a] = low ? 'min' : 'max'\n else ((next.align[a] = 'mm'), (next.alignMm[a] = Math.round(edge)))\n }\n return next.moved.length ? next : { cannot: 'inside' }\n}\n\n/** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */\nexport function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {\n const sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> = []\n let touching = true\n for (const a of view.axes) {\n const face = view.face[a]\n if (face === undefined) continue\n const lo = at[a] - view.part[a] / 2,\n hi = at[a] + view.part[a] / 2\n if (lo >= face / 2 || hi <= -face / 2) touching = false\n if (-face / 2 - lo > 0.5) sides.push({ axis: a, side: 'low', mm: Math.round(-face / 2 - lo) })\n if (hi - face / 2 > 0.5) sides.push({ axis: a, side: 'high', mm: Math.round(hi - face / 2) })\n }\n return { touching, sides }\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' || ('face' in j.body1 && j.body1.face === '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"]}
@@ -54,6 +54,8 @@ export declare class FigureLinks extends FigureLinks_base {
54
54
  /** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
55
55
  private spotlit;
56
56
  private fasten;
57
+ /** Why 정렬·간격 고치기 moved nothing, said under it; empty once it moved the part or the part changed. */
58
+ private fixNote;
57
59
  private motion;
58
60
  private place;
59
61
  /** How places are made here: one at a time, or by the figure's rule. Unset: by the rule when the figure has one. */
@@ -62,7 +64,7 @@ export declare class FigureLinks extends FigureLinks_base {
62
64
  /** The motion form is open over the part's own motion, to change it where it is. */
63
65
  private editingMotion;
64
66
  private loadedFor?;
65
- willUpdate(_changed: Map<string, unknown>): void;
67
+ willUpdate(changed: Map<string, unknown>): void;
66
68
  updated(): void;
67
69
  disconnectedCallback(): void;
68
70
  render(): import("lit-html").TemplateResult<1> | typeof nothing;
@@ -78,7 +80,12 @@ export declare class FigureLinks extends FigureLinks_base {
78
80
  * holding places instead).
79
81
  */
80
82
  /** The face the part sits on, in view with the focus on it: where the place across the face is dragged or keyed. */
81
- private showWhere;
83
+ /**
84
+ * 정렬·간격 고치기: the part moved inside the face it is fastened on, as one step -- the least move on each side it is
85
+ * out, or centred when it is wider than the face. When nothing of it can be moved, why is said under the button, not
86
+ * nothing at all (:3300, 2026-10-11: the button only scrolled to the drawing, which was already in view).
87
+ */
88
+ private fixAlignment;
82
89
  /** The fastening form, open on this part and in view: for "fasten it first" from elsewhere. */
83
90
  showFastening(): Promise<void>;
84
91
  private openFastening;
@@ -5,11 +5,11 @@ import '@operato/input/ox-input-select-buttons.js';
5
5
  import './figure-slot-layout-panel.js';
6
6
  import { customElement, property, state } from 'lit/decorators.js';
7
7
  import { i18next, localize } from '@operato/i18n';
8
- import { FIGURE_FACES } from '@hatiolab/figure-model';
8
+ import { FIGURE_FACES, figureFaceOverhangOf } from '@hatiolab/figure-model';
9
9
  import { acceptsOf, alignOf, AXES, canFasten, facesOf, canFollow, followCommandOf, followableAxes, positionIsShare, choiceOf, heldBy, followOf, hangsFrom, shownWhenOf, visibilityControlsOf, placeKeptOf, fasteningOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionFormOf, motionFormRefusalOf, endPlaceKeyOf, motionJoint, placeFacesOf, placeIdFor, placesOn, rowAxesOf, rowBlockOf, fixedJointOf, rowOf, positionsWorkedOut, fieldWordsOf, measureNow, ownFacesOf, alignRowsOf } from './links.js';
10
10
  import { SidePanelStyles } from './side-panel-styles.js';
11
11
  import './figure-fasten-drawing.js';
12
- import { faceViewOf, modeOf, myFaceFor, turnsInPlace } from './fasten-geometry.js';
12
+ import { faceViewOf, insideAlignmentOf, modeOf, myFaceFor, turnsInPlace } from './fasten-geometry.js';
13
13
  import { HelpToggles } from './help.js';
14
14
  import { jointAxisWord } from './joint-axis.js';
15
15
  const FACES = Object.keys(FIGURE_FACES);
@@ -70,6 +70,8 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
70
70
  /** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
71
71
  this.spotlit = false;
72
72
  this.fasten = freshFasten();
73
+ /** Why 정렬·간격 고치기 moved nothing, said under it; empty once it moved the part or the part changed. */
74
+ this.fixNote = '';
73
75
  this.motion = freshMotion();
74
76
  this.place = freshPlace();
75
77
  this.row = freshRow();
@@ -131,7 +133,10 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
131
133
  get asset() {
132
134
  return this.revision?.asset;
133
135
  }
134
- willUpdate(_changed) {
136
+ willUpdate(changed) {
137
+ /* What 고치기 said is about the figure as it was: another step or another part, and it no longer holds. */
138
+ if (changed.has('asset') || changed.has('partId'))
139
+ this.fixNote = '';
135
140
  if (this.asset && this.partId && this.partId !== this.loadedFor) {
136
141
  this.loadedFor = this.partId;
137
142
  /* A part's own fastening is not another part's choices: the form opens on it (walk 1, 2026-09-26). */
@@ -348,17 +353,26 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
348
353
  * holding places instead).
349
354
  */
350
355
  /** The face the part sits on, in view with the focus on it: where the place across the face is dragged or keyed. */
351
- async showWhere() {
352
- this.working = 'fasten';
353
- await this.updateComplete;
354
- /* The drawing draws itself after this panel does: its "where" is there only once it has. */
355
- const drawing = this.renderRoot.querySelector('figure-fasten-drawing');
356
- await drawing?.updateComplete;
357
- const where = drawing?.shadowRoot?.querySelector('div[where]');
358
- if (!where)
356
+ /**
357
+ * 정렬·간격 고치기: the part moved inside the face it is fastened on, as one step -- the least move on each side it is
358
+ * out, or centred when it is wider than the face. When nothing of it can be moved, why is said under the button, not
359
+ * nothing at all (:3300, 2026-10-11: the button only scrolled to the drawing, which was already in view).
360
+ */
361
+ async fixAlignment(view, target) {
362
+ /* Whether it is out is the kernel's measure (a round face is not measured there); the drawing's sizes say how far to move. */
363
+ const own = this.revision ? figureFaceOverhangOf(this.revision.asset, this.partId, this.revision.inputs) : undefined;
364
+ const fixed = own?.state === 'unknown'
365
+ ? { cannot: 'unmeasured' }
366
+ : own?.state === 'out' || own?.state === 'possible'
367
+ ? insideAlignmentOf(view, this.fasten.align, this.fasten.alignMm)
368
+ : { cannot: 'inside' };
369
+ if ('cannot' in fixed) {
370
+ this.fixNote = i18next.t(`figure.text.fix-alignment-${fixed.cannot}`, { part: this.partId, on: target });
359
371
  return;
360
- where.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
361
- where.focus({ preventScroll: true });
372
+ }
373
+ this.fixNote = '';
374
+ this.fasten = { ...this.fasten, align: fixed.align, alignMm: fixed.alignMm };
375
+ await this.onFastenRelease(true);
362
376
  }
363
377
  /** The fastening form, open on this part and in view: for "fasten it first" from elsewhere. */
364
378
  async showFastening() {
@@ -512,7 +526,10 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
512
526
  ? html `<div overhang role="status">
513
527
  ${this.overhangWords.map(line => html `<p>${line}</p>`)}
514
528
  ${current
515
- ? html `<button act @click=${() => this.showWhere()}><md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}</button>`
529
+ ? html `<button act fix-alignment ?disabled=${this.locked} @click=${() => this.fixAlignment(view, held ? f.target : '')}>
530
+ <md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}
531
+ </button>
532
+ ${this.fixNote ? html `<p hint fix-note>${this.fixNote}</p>` : nothing}`
516
533
  : nothing}
517
534
  </div>`
518
535
  : nothing}
@@ -965,6 +982,10 @@ __decorate([
965
982
  state(),
966
983
  __metadata("design:type", Object)
967
984
  ], FigureLinks.prototype, "fasten", void 0);
985
+ __decorate([
986
+ state(),
987
+ __metadata("design:type", Object)
988
+ ], FigureLinks.prototype, "fixNote", void 0);
968
989
  __decorate([
969
990
  state(),
970
991
  __metadata("design:type", Object)