q
This commit is contained in:
parent
810ad6f5cb
commit
de7c6c32bd
123 changed files with 13201 additions and 6213 deletions
|
|
@ -11,6 +11,7 @@ import {
|
|||
normalizePatchPrefectureCapitals,
|
||||
preparePatchTransactionFields,
|
||||
reconcileGeneratedHumanPointsWithFinalTerrain,
|
||||
regionalRecalculationProbability,
|
||||
restorePatchTransactionSnapshot,
|
||||
refreshPatchPrefectureMetadata,
|
||||
synchronizePatchAdministrativeMetadata,
|
||||
|
|
@ -37,6 +38,16 @@ function assert(condition, message) {
|
|||
console.log(`OK: ${message}`);
|
||||
}
|
||||
|
||||
const recalcAtSelection = regionalRecalculationProbability(0, 100);
|
||||
const recalcNear = regionalRecalculationProbability(20, 100);
|
||||
const recalcFar = regionalRecalculationProbability(70, 100);
|
||||
const recalcOutside = regionalRecalculationProbability(100, 100);
|
||||
assert(recalcAtSelection === 1
|
||||
&& recalcNear > recalcFar
|
||||
&& recalcFar > 0
|
||||
&& recalcOutside === 0,
|
||||
"regional recalculation probability rises monotonically toward the selected expansion area");
|
||||
|
||||
function testPointInPolygon(px, py, polygon) {
|
||||
let inside = false;
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
|
|
@ -80,6 +91,66 @@ assert(search.result?.ok && search.result.actualVariant === 6, "content rejectio
|
|||
assert(calls.length === 2 && calls.every((call) => call.baseline === 1), "candidate worlds start from one immutable baseline");
|
||||
assert(progress.filter((event) => event.boundedWork).every((event) => event.completed <= event.total), "bounded progress remains inside its finite work total");
|
||||
|
||||
const bestOfThreeCalls = [];
|
||||
const bestOfThreeScores = new Map([[12, 0.31], [13, 0.88], [14, 0.57]]);
|
||||
const bestOfThree = runPatchCandidateSearch({
|
||||
id: 1001,
|
||||
world: base,
|
||||
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-best-of-three",
|
||||
selectBestCandidate: true,
|
||||
candidatePlan: [12, 13, 14].map((variant, index) => ({ candidateOrdinal: index + 1, candidateId: `best-${variant}`, variant, seed: 100 + variant })),
|
||||
},
|
||||
}, {
|
||||
cloneWorld: structuredClone,
|
||||
generateCandidate: (world, rect, options) => {
|
||||
bestOfThreeCalls.push(options.variant);
|
||||
world.fields.marker[0] = options.variant;
|
||||
return {
|
||||
ok: true,
|
||||
variant: options.variant,
|
||||
seed: options.seed,
|
||||
candidateQuality: { hardPass: false, score: bestOfThreeScores.get(options.variant) },
|
||||
seamDiagnostics: { hardPass: false, gateReasons: ["forced-diagnostic-only-seam"] },
|
||||
rects: { writeRect: rect },
|
||||
};
|
||||
},
|
||||
});
|
||||
assert(bestOfThree.result?.ok
|
||||
&& bestOfThree.result.bestOfCandidates === true
|
||||
&& bestOfThree.result.actualVariant === 13
|
||||
&& bestOfThree.result.candidateOrdinal === 2
|
||||
&& bestOfThreeCalls.join(",") === "12,13,14"
|
||||
&& bestOfThree.result.searchAttempts.map((attempt) => attempt.status).join(",") === "evaluated,success,evaluated",
|
||||
"best-of-three mode evaluates every complete candidate and publishes the highest quality even when all gates remain diagnostic failures");
|
||||
|
||||
const asyncBestOfThree = await runPatchCandidateSearchAsync({
|
||||
id: 1002,
|
||||
world: base,
|
||||
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-best-of-three-async",
|
||||
selectBestCandidate: true,
|
||||
candidatePlan: [21, 22, 23].map((variant, index) => ({ candidateOrdinal: index + 1, candidateId: `async-best-${variant}`, variant, seed: 200 + variant })),
|
||||
},
|
||||
}, {
|
||||
cloneWorld: structuredClone,
|
||||
generateCandidate: async (world, rect, options) => ({
|
||||
ok: true,
|
||||
variant: options.variant,
|
||||
seed: options.seed,
|
||||
candidateQuality: { hardPass: options.variant === 22, score: options.variant === 23 ? 0.93 : options.variant === 22 ? 0.71 : 0.42 },
|
||||
seamDiagnostics: { hardPass: options.variant === 22, gateReasons: options.variant === 22 ? [] : ["diagnostic-seam"] },
|
||||
rects: { writeRect: rect },
|
||||
}),
|
||||
});
|
||||
assert(asyncBestOfThree.result?.ok && asyncBestOfThree.result.actualVariant === 23
|
||||
&& asyncBestOfThree.result.searchAttempts.length === 3,
|
||||
"async best-of-three selection uses quality score as the primary ranking signal and evaluates all candidates");
|
||||
|
||||
const repeatedPipelineProgress = [];
|
||||
const repeatedPipelineSearch = runPatchCandidateSearch({
|
||||
id: 101,
|
||||
|
|
@ -383,6 +454,18 @@ assert(!ArrayBuffer.isView(transportDebugViewport.transportDebug.layers.expressw
|
|||
&& transportDebugViewport.transportDebug.layers.components.length === 1,
|
||||
"viewport drops unused transport potential rasters while preserving vector diagnostics");
|
||||
|
||||
const edgeViewport = getViewportMap({
|
||||
width: 3,
|
||||
height: 2,
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
renderRevision: 1,
|
||||
fields: { sea: new Uint8Array([1, 2, 3, 4, 5, 6]) },
|
||||
sourceMap: {},
|
||||
}, { x: -1, y: -1 }, 4, 4);
|
||||
assert(Array.from(edgeViewport.sea).join(",") === "1,1,1,1,1,1,2,3,1,4,5,6,1,1,1,1",
|
||||
"viewport row copies preserve exact clipping and fallback cells at negative camera edges");
|
||||
|
||||
const arrayDeltaBase = {
|
||||
width: 1, height: 1,
|
||||
fields: { marker: new Uint8Array([1]) }, generatedMask: new Uint8Array(1),
|
||||
|
|
@ -540,6 +623,264 @@ assert(transactionalCalls.every(([sea, municipality, villages]) => sea === 0 &&
|
|||
&& JSON.stringify(transactionalSearchBase) === JSON.stringify(transactionOriginal),
|
||||
"rejected and accepted transactional candidates both start from and restore the same committed mirror");
|
||||
|
||||
const transactionalBestBase = structuredClone(transactionOriginal);
|
||||
const transactionalBestScores = new Map([[31, 0.44], [32, 0.91], [33, 0.68]]);
|
||||
const transactionalBestSearch = runPatchCandidateSearch({
|
||||
id: 303,
|
||||
world: transactionalBestBase,
|
||||
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
selectBestCandidate: true,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan: [31, 32, 33].map((variant, index) => ({ candidateOrdinal: index + 1, candidateId: `transactional-best-${variant}`, variant, seed: 300 + variant })),
|
||||
},
|
||||
}, {
|
||||
transactional: true,
|
||||
generateCandidate: (world, rect, options) => {
|
||||
preparePatchTransactionFields(options._externalTransactionSnapshot, world, rect);
|
||||
world.fields.sea[19] = options.variant;
|
||||
world.fields.municipalityId[10] = options.variant;
|
||||
world.sourceMap.villages.push({ x: options.variant, y: 2 });
|
||||
world.patchGenerationSerial++;
|
||||
return {
|
||||
ok: true,
|
||||
rects: { writeRect: rect },
|
||||
candidateQuality: { hardPass: false, score: transactionalBestScores.get(options.variant) },
|
||||
seamDiagnostics: { hardPass: false, gateReasons: ["diagnostic-only"] },
|
||||
};
|
||||
},
|
||||
});
|
||||
const transactionalBestApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), transactionalBestSearch.transactionDelta);
|
||||
assert(transactionalBestSearch.result?.actualVariant === 32
|
||||
&& transactionalBestSearch.result?.candidateOrdinal === 2
|
||||
&& JSON.stringify(transactionalBestBase) === JSON.stringify(transactionOriginal)
|
||||
&& transactionalBestApplied.fields.sea[19] === 32
|
||||
&& transactionalBestApplied.fields.municipalityId[10] === 32
|
||||
&& transactionalBestApplied.sourceMap.villages.at(-1)?.x === 32,
|
||||
"transactional best-of-three keeps only the highest-quality candidate delta and restores the committed mirror after every evaluation");
|
||||
|
||||
const draftSelectionBase = structuredClone(transactionOriginal);
|
||||
const draftSelectionScores = new Map([[41, 0.41], [42, 0.94], [43, 0.63]]);
|
||||
const draftSelectionDraftCalls = [];
|
||||
const draftSelectionFullCalls = [];
|
||||
let draftSelectionDeltaBuilds = 0;
|
||||
let draftSelectionHashBuilds = 0;
|
||||
const draftSelectionProgress = [];
|
||||
const draftSelectionSearch = await runPatchCandidateSearchAsync({
|
||||
id: 304,
|
||||
world: draftSelectionBase,
|
||||
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-draft-ranked-production",
|
||||
selectBestCandidate: true,
|
||||
draftSelection: true,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan: [41, 42, 43].map((variant, index) => ({
|
||||
candidateOrdinal: index + 1, candidateId: `draft-ranked-${variant}`, variant, seed: 400 + variant,
|
||||
})),
|
||||
},
|
||||
}, {
|
||||
transactional: true,
|
||||
onProgress: (message) => draftSelectionProgress.push(message.progress),
|
||||
prepareOperationContext: () => ({ ok: true, signature: "unit-shared-context" }),
|
||||
evaluateDraftCandidate: async (world, rect, options) => {
|
||||
draftSelectionDraftCalls.push(options.variant);
|
||||
return {
|
||||
ok: true,
|
||||
score: draftSelectionScores.get(options.variant),
|
||||
qualityUpperBound: draftSelectionScores.get(options.variant),
|
||||
candidateQuality: { score: draftSelectionScores.get(options.variant), qualityUpperBound: draftSelectionScores.get(options.variant), terrain: { score: 0.8 }, human: { score: 0.7 } },
|
||||
reusableForFull: true,
|
||||
precomputedDraft: { terrain: { unitTerrainVariant: options.variant }, baseSeed: (400 + options.variant) >>> 0, effectiveSeed: (400 + options.variant) >>> 0, generationContext: { variant: options.variant }, generationTimings: [{ key: "terrain", ms: 1 }] },
|
||||
};
|
||||
},
|
||||
generateCandidate: async (world, rect, options) => {
|
||||
draftSelectionFullCalls.push({ variant: options.variant, reusedFullDraft: options._precomputedDraftCandidate != null, reusedTerrain: options._precomputedTerrainDraftCandidate?.generationContext?.variant ?? null });
|
||||
preparePatchTransactionFields(options._externalTransactionSnapshot, world, rect);
|
||||
world.fields.sea[19] = options.variant;
|
||||
world.fields.municipalityId[10] = options.variant;
|
||||
world.sourceMap.villages.push({ x: options.variant, y: 3 });
|
||||
world.patchGenerationSerial++;
|
||||
return {
|
||||
ok: true,
|
||||
rects: { writeRect: rect },
|
||||
candidateQuality: { hardPass: true, score: 0.9 },
|
||||
seamDiagnostics: { hardPass: true },
|
||||
};
|
||||
},
|
||||
buildCommittedDeltaFromTransaction: (transaction, world) => {
|
||||
draftSelectionDeltaBuilds++;
|
||||
return buildCommittedMirrorDeltaFromTransaction(transaction, world);
|
||||
},
|
||||
hashCommittedWorld: (world) => {
|
||||
draftSelectionHashBuilds++;
|
||||
return hashCommittedWorld(world);
|
||||
},
|
||||
});
|
||||
const draftSelectionApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), draftSelectionSearch.transactionDelta);
|
||||
assert(draftSelectionSearch.result?.ok
|
||||
&& draftSelectionSearch.result.actualVariant === 42
|
||||
&& draftSelectionSearch.result.draftSelection?.policy === "admissible-terrain-scout-branch-and-bound-two-lane-v2"
|
||||
&& draftSelectionSearch.result.draftSelection?.fullCandidateCount === 1
|
||||
&& draftSelectionSearch.result.draftSelection?.reusedWinningDraft === false
|
||||
&& draftSelectionSearch.result.draftSelection?.reusedTerrainDraft === true
|
||||
&& draftSelectionSearch.result.draftSelection?.fullProductionFromTerrainOnly === true
|
||||
&& draftSelectionDraftCalls.join(",") === "41,42,43"
|
||||
&& draftSelectionFullCalls.length === 1
|
||||
&& draftSelectionFullCalls[0].variant === 42
|
||||
&& draftSelectionFullCalls[0].reusedFullDraft === false
|
||||
&& draftSelectionFullCalls[0].reusedTerrain === 42,
|
||||
"terrain-scout production evaluates three cheap terrain candidates, fully finalizes only the viable winner, and never reuses simplified human/transport draft stages");
|
||||
assert(draftSelectionDeltaBuilds === 1
|
||||
&& draftSelectionHashBuilds === 1
|
||||
&& draftSelectionApplied.fields.sea[19] === 42
|
||||
&& draftSelectionApplied.fields.municipalityId[10] === 42
|
||||
&& JSON.stringify(draftSelectionBase) === JSON.stringify(transactionOriginal),
|
||||
"draft-ranked production builds the committed delta and whole-world hash exactly once for the accepted finalist and restores the committed mirror");
|
||||
assert(draftSelectionProgress.filter((event) => event.boundedWork).every((event) => event.completed <= event.total)
|
||||
&& draftSelectionProgress.filter((event) => event.workUnitId?.startsWith("finalist-") && event.key === "finalist-generation").every((event) => event.total === 1),
|
||||
"draft-ranked production keeps finalist progress bounded per full candidate without leaving an incomplete multi-finalist work unit");
|
||||
|
||||
const nearTieBase = structuredClone(transactionOriginal);
|
||||
const nearTieDraftScores = new Map([[51, 0.900], [52, 0.885], [53, 0.61]]);
|
||||
const nearTieFullScores = new Map([[51, 0.78], [52, 0.93], [53, 0.64]]);
|
||||
const nearTieFullCalls = [];
|
||||
let nearTieDeltaBuilds = 0;
|
||||
let nearTieHashBuilds = 0;
|
||||
const nearTieSearch = await runPatchCandidateSearchAsync({
|
||||
id: 305,
|
||||
world: nearTieBase,
|
||||
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-draft-near-tie",
|
||||
selectBestCandidate: true,
|
||||
draftSelection: true,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan: [51, 52, 53].map((variant, index) => ({
|
||||
candidateOrdinal: index + 1, candidateId: `draft-near-${variant}`, variant, seed: 500 + variant,
|
||||
})),
|
||||
},
|
||||
}, {
|
||||
transactional: true,
|
||||
prepareOperationContext: () => ({ ok: true, signature: "unit-near-tie-context" }),
|
||||
evaluateDraftCandidate: async (world, rect, options) => {
|
||||
const upper = new Map([[51, 0.95], [52, 0.96], [53, 0.70]]).get(options.variant);
|
||||
return {
|
||||
ok: true,
|
||||
score: nearTieDraftScores.get(options.variant),
|
||||
qualityUpperBound: upper,
|
||||
candidateQuality: { score: nearTieDraftScores.get(options.variant), qualityUpperBound: upper, hardPass: true, terrain: { score: 0.8 }, human: { score: 0.7 } },
|
||||
reusableForFull: true,
|
||||
precomputedDraft: { terrain: { unitTerrainVariant: options.variant }, baseSeed: (500 + options.variant) >>> 0, effectiveSeed: (500 + options.variant) >>> 0, generationContext: { variant: options.variant }, generationTimings: [{ key: "terrain", ms: 1 }] },
|
||||
};
|
||||
},
|
||||
generateCandidate: async (world, rect, options) => {
|
||||
nearTieFullCalls.push({ variant: options.variant, reusedFullDraft: options._precomputedDraftCandidate != null, reusedTerrain: options._precomputedTerrainDraftCandidate?.generationContext?.variant ?? null });
|
||||
preparePatchTransactionFields(options._externalTransactionSnapshot, world, rect);
|
||||
world.fields.sea[19] = options.variant;
|
||||
world.fields.municipalityId[10] = options.variant;
|
||||
world.sourceMap.villages.push({ x: options.variant, y: 7 });
|
||||
world.patchGenerationSerial++;
|
||||
return {
|
||||
ok: true,
|
||||
rects: { writeRect: rect },
|
||||
candidateQuality: { hardPass: true, score: nearTieFullScores.get(options.variant) },
|
||||
seamDiagnostics: { hardPass: true, gateReasons: [] },
|
||||
};
|
||||
},
|
||||
buildCommittedDeltaFromTransaction: (transaction, world) => {
|
||||
nearTieDeltaBuilds++;
|
||||
return buildCommittedMirrorDeltaFromTransaction(transaction, world);
|
||||
},
|
||||
hashCommittedWorld: (world) => {
|
||||
nearTieHashBuilds++;
|
||||
return hashCommittedWorld(world);
|
||||
},
|
||||
});
|
||||
const nearTieApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), nearTieSearch.transactionDelta);
|
||||
assert(nearTieSearch.result?.ok
|
||||
&& nearTieSearch.result.actualVariant === 52
|
||||
&& nearTieSearch.result.draftSelection?.policy === "admissible-terrain-scout-branch-and-bound-two-lane-v2"
|
||||
&& nearTieSearch.result.draftSelection?.selectionReason === "admissible-bound-winner-live"
|
||||
&& nearTieSearch.result.draftSelection?.fullCandidateCount === 2
|
||||
&& nearTieSearch.result.draftSelection?.fullGenerationPassCount === 2
|
||||
&& nearTieSearch.result.draftSelection?.branchBoundPrunedCandidateOrdinals.includes(3)
|
||||
&& nearTieFullCalls.map((row) => row.variant).join(",") === "51,52",
|
||||
"admissible Branch-and-Bound fully evaluates only candidates that can still beat the current best and removes the fixed near-tie heuristic");
|
||||
assert(nearTieDeltaBuilds === 1
|
||||
&& nearTieHashBuilds === 1
|
||||
&& nearTieApplied.fields.sea[19] === 52
|
||||
&& nearTieApplied.fields.municipalityId[10] === 52
|
||||
&& JSON.stringify(nearTieBase) === JSON.stringify(transactionOriginal),
|
||||
"near-tie comparison still constructs delta/hash only once for the final selected candidate and restores the committed mirror");
|
||||
|
||||
const draftFallbackBase = structuredClone(transactionOriginal);
|
||||
const draftFallbackCalls = [];
|
||||
let draftFallbackDeltaBuilds = 0;
|
||||
let draftFallbackHashBuilds = 0;
|
||||
const draftFallbackSearch = await runPatchCandidateSearchAsync({
|
||||
id: 306,
|
||||
world: draftFallbackBase,
|
||||
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-draft-content-fallback",
|
||||
selectBestCandidate: true,
|
||||
draftSelection: true,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan: [71, 72, 73].map((variant, index) => ({
|
||||
candidateOrdinal: index + 1, candidateId: `draft-fallback-${variant}`, variant, seed: 700 + variant,
|
||||
})),
|
||||
},
|
||||
}, {
|
||||
transactional: true,
|
||||
prepareOperationContext: () => ({ ok: true, signature: "unit-fallback-context" }),
|
||||
evaluateDraftCandidate: async (world, rect, options) => ({
|
||||
ok: true,
|
||||
score: options.variant === 71 ? 0.91 : options.variant === 72 ? 0.72 : 0.51,
|
||||
qualityUpperBound: options.variant === 71 ? 0.96 : options.variant === 72 ? 0.90 : 0.60,
|
||||
candidateQuality: { score: options.variant === 71 ? 0.91 : options.variant === 72 ? 0.72 : 0.51, qualityUpperBound: options.variant === 71 ? 0.96 : options.variant === 72 ? 0.90 : 0.60, hardPass: true },
|
||||
reusableForFull: true,
|
||||
precomputedDraft: { terrain: { unitTerrainVariant: options.variant }, baseSeed: (700 + options.variant) >>> 0, effectiveSeed: (700 + options.variant) >>> 0, generationContext: { variant: options.variant }, generationTimings: [{ key: "terrain", ms: 1 }] },
|
||||
}),
|
||||
generateCandidate: async (world, rect, options) => {
|
||||
draftFallbackCalls.push({ variant: options.variant, reusedFullDraft: options._precomputedDraftCandidate != null, reusedTerrain: options._precomputedTerrainDraftCandidate?.generationContext?.variant ?? null });
|
||||
preparePatchTransactionFields(options._externalTransactionSnapshot, world, rect);
|
||||
if (options.variant === 71) return { ok: false, code: "patch-quality-gate-failed", reason: "forced full-stage rejection" };
|
||||
world.fields.sea[19] = options.variant;
|
||||
world.fields.municipalityId[10] = options.variant;
|
||||
world.patchGenerationSerial++;
|
||||
return { ok: true, rects: { writeRect: rect }, candidateQuality: { hardPass: true, score: 0.82 }, seamDiagnostics: { hardPass: true } };
|
||||
},
|
||||
buildCommittedDeltaFromTransaction: (transaction, world) => {
|
||||
draftFallbackDeltaBuilds++;
|
||||
return buildCommittedMirrorDeltaFromTransaction(transaction, world);
|
||||
},
|
||||
hashCommittedWorld: (world) => {
|
||||
draftFallbackHashBuilds++;
|
||||
return hashCommittedWorld(world);
|
||||
},
|
||||
});
|
||||
const draftFallbackApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), draftFallbackSearch.transactionDelta);
|
||||
assert(draftFallbackSearch.result?.ok
|
||||
&& draftFallbackSearch.result.actualVariant === 72
|
||||
&& draftFallbackSearch.result.draftSelection?.selectionReason?.startsWith("admissible-bound")
|
||||
&& draftFallbackSearch.result.draftSelection?.fullCandidateCount === 2
|
||||
&& draftFallbackCalls.map((row) => row.variant).join(",") === "71,72"
|
||||
&& draftFallbackCalls[0].reusedFullDraft === false
|
||||
&& draftFallbackCalls[0].reusedTerrain === 71
|
||||
&& draftFallbackCalls[1].reusedFullDraft === false
|
||||
&& draftFallbackCalls[1].reusedTerrain === null,
|
||||
"clear terrain-scout winner reuses only exact terrain on the fast path and regenerates a discarded runner-up after content rejection");
|
||||
assert(draftFallbackDeltaBuilds === 1
|
||||
&& draftFallbackHashBuilds === 1
|
||||
&& draftFallbackApplied.fields.sea[19] === 72
|
||||
&& JSON.stringify(draftFallbackBase) === JSON.stringify(transactionOriginal),
|
||||
"content fallback preserves the one-delta/one-hash publication invariant");
|
||||
|
||||
const municipalityWorld = {
|
||||
width: 8, height: 6,
|
||||
fields: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue