Match3D & Puzzle: runtime UI, assets, drag fix

Backend: stop treating background music as a required draft asset and remove auto-submit/plan for background music; load persisted generated UI/assets into Match3D agent session responses (added helpers to resolve profile id and fetch existing generated assets). Frontend: make Match3D result preview reuse runtime UI styles, unify runtime settings entry, update PuzzleRuntime to apply immediate pointermove transforms (disable drag transition), use SVG clipPath for merged piece rounding, ensure PuzzleRuntimeShell supplies platform theme classes, and adjust related tests. Docs & logs: update decision log, pitfalls and product docs to reflect these changes.
This commit is contained in:
2026-05-15 08:49:59 +08:00
parent 0f36beee91
commit bb60ca91ef
23 changed files with 2127 additions and 593 deletions

View File

@@ -18,6 +18,7 @@ type GridEdge = {
};
const MERGED_GROUP_OUTLINE_CORNER_RADIUS = 0.16;
const MERGED_GROUP_CONCAVE_CORNER_RADIUS_FACTOR = 1.2;
function buildLocalCellKey(row: number, col: number) {
return `${row}:${col}`;
@@ -78,15 +79,38 @@ function removeCollinearGridPoints(points: GridPoint[]) {
});
}
function computePolygonSignedArea(points: GridPoint[]) {
let area = 0;
for (let index = 0; index < points.length; index += 1) {
const current = points[index];
const next = points[(index + 1) % points.length];
if (!current || !next) {
continue;
}
area += current.x * next.y - next.x * current.y;
}
return area / 2;
}
type CornerRadiusResolver = (corner: {
point: GridPoint;
previous: GridPoint;
next: GridPoint;
isConvex: boolean;
radius: number;
}) => number;
function buildRoundedGridCyclePath(
points: GridPoint[],
radius: number,
transformPoint: (point: GridPoint) => GridPoint = (point) => point,
resolveCornerRadius?: CornerRadiusResolver,
) {
const cyclePoints = removeCollinearGridPoints(points);
if (cyclePoints.length < 3) {
return '';
}
const polygonOrientation = computePolygonSignedArea(cyclePoints) >= 0 ? 1 : -1;
const resolveCorner = (index: number) => {
const point = cyclePoints[index];
const previous = cyclePoints[
@@ -96,8 +120,25 @@ function buildRoundedGridCyclePath(
if (!point || !previous || !next) {
return null;
}
const previousVectorX = point.x - previous.x;
const previousVectorY = point.y - previous.y;
const nextVectorX = next.x - point.x;
const nextVectorY = next.y - point.y;
const turnCross =
previousVectorX * nextVectorY - previousVectorY * nextVectorX;
const isConvex = turnCross * polygonOrientation > 0;
const resolvedRadius = Math.max(
0,
resolveCornerRadius?.({
point,
previous,
next,
isConvex,
radius,
}) ?? radius,
);
const safeRadius = Math.min(
radius,
resolvedRadius,
distanceBetweenGridPoints(point, previous) / 2,
distanceBetweenGridPoints(point, next) / 2,
);
@@ -216,13 +257,34 @@ function buildMergedGroupBoundaryCycles(group: PuzzleMergedGroupShape) {
return cycles;
}
export function buildRoundedGridCellClipPath(
radius = MERGED_GROUP_OUTLINE_CORNER_RADIUS,
) {
return buildRoundedGridCyclePath([
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 1 },
{ x: 0, y: 1 },
], radius);
}
export function buildMergedGroupOutlinePath(
group: PuzzleMergedGroupShape,
radius = MERGED_GROUP_OUTLINE_CORNER_RADIUS,
) {
// 合并块的凹入角不能靠单格 border-radius 稳定拼出来,必须先生成整体外轮廓。
return buildMergedGroupBoundaryCycles(group)
.map((cycle) => buildRoundedGridCyclePath(cycle, radius))
.map((cycle) =>
buildRoundedGridCyclePath(
cycle,
radius,
(corner) => corner,
({ isConvex }) =>
isConvex
? radius
: radius * MERGED_GROUP_CONCAVE_CORNER_RADIUS_FACTOR,
),
)
.filter(Boolean)
.join(' ');
}
@@ -233,10 +295,18 @@ export function buildMergedGroupClipPath(
) {
return buildMergedGroupBoundaryCycles(group)
.map((cycle) =>
buildRoundedGridCyclePath(cycle, radius, (point) => ({
x: point.x / group.colSpan,
y: point.y / group.rowSpan,
})),
buildRoundedGridCyclePath(
cycle,
radius,
(point) => ({
x: point.x / group.colSpan,
y: point.y / group.rowSpan,
}),
({ isConvex }) =>
isConvex
? radius
: radius * MERGED_GROUP_CONCAVE_CORNER_RADIUS_FACTOR,
),
)
.filter(Boolean)
.join(' ');