Clarify story stage presence and portraits
This commit is contained in:
parent
35c56d2c2c
commit
9bb738f07e
@ -1190,7 +1190,8 @@ static void Phase21RSceneBrowserAndPanelsAreReplayFriendly()
|
|||||||
Assert(!view.Contains("data-assets", StringComparison.Ordinal), "Asset visualisation lane should not be rendered on the main stage.");
|
Assert(!view.Contains("data-assets", StringComparison.Ordinal), "Asset visualisation lane should not be rendered on the main stage.");
|
||||||
Assert(script.Contains("Assets are recorded in insights/diagnostics but not rendered as stage nodes.", StringComparison.Ordinal), "Asset data should remain available without drawing asset stage nodes.");
|
Assert(script.Contains("Assets are recorded in insights/diagnostics but not rendered as stage nodes.", StringComparison.Ordinal), "Asset data should remain available without drawing asset stage nodes.");
|
||||||
Assert(script.Contains("width * 0.84 - size / 2", StringComparison.Ordinal), "Location should be a compact right-side anchor once asset nodes are removed.");
|
Assert(script.Contains("width * 0.84 - size / 2", StringComparison.Ordinal), "Location should be a compact right-side anchor once asset nodes are removed.");
|
||||||
Assert(script.Contains("{ x: 0.52, y: 0.66, role: \"context\" }", StringComparison.Ordinal), "Character layout should use more of the stage width.");
|
Assert(script.Contains("{ x: 0.60, y: 0.66, role: \"context\" }", StringComparison.Ordinal), "Character layout should use more of the stage width.");
|
||||||
|
Assert(script.Contains("presence-cluster", StringComparison.Ordinal), "Presence should be shown as a single location callout instead of many confusing location lines.");
|
||||||
Assert(css.Contains(".story-exp-zone--location .story-exp-zone-label {\n left: 80%;", StringComparison.Ordinal), "Location lane label should follow the compact right-side anchor.");
|
Assert(css.Contains(".story-exp-zone--location .story-exp-zone-label {\n left: 80%;", StringComparison.Ordinal), "Location lane label should follow the compact right-side anchor.");
|
||||||
Assert(!css.Contains("grid-template-rows: minmax(218px, 1.35fr) minmax(185px, 1.08fr) auto", StringComparison.Ordinal), "Right rail must not use fixed-height panel tracks.");
|
Assert(!css.Contains("grid-template-rows: minmax(218px, 1.35fr) minmax(185px, 1.08fr) auto", StringComparison.Ordinal), "Right rail must not use fixed-height panel tracks.");
|
||||||
Assert(!css.Contains("grid-template-rows: auto auto minmax(170px, 1fr) minmax(96px, 0.56fr)", StringComparison.Ordinal), "Left rail must not use fixed-height panel tracks.");
|
Assert(!css.Contains("grid-template-rows: auto auto minmax(170px, 1fr) minmax(96px, 0.56fr)", StringComparison.Ordinal), "Left rail must not use fixed-height panel tracks.");
|
||||||
|
|||||||
@ -81,7 +81,8 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
|||||||
- subtle depth of field, clean modern rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
|
- subtle depth of field, clean modern rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
|
||||||
- no visible text, captions, logos, watermarks, UI, signatures, or typography
|
- no visible text, captions, logos, watermarks, UI, signatures, or typography
|
||||||
- no anime, cartoon, Pixar-like, caricature, celebrity likeness, or photoreal identity claim
|
- no anime, cartoon, Pixar-like, caricature, celebrity likeness, or photoreal identity claim
|
||||||
- for character images, the specified hair colour, hair length, apparent age, era, location context and skin tone are hard requirements; do not reinterpret or diversify away from them
|
- for character images, the specified hair colour, hair length, apparent age, era, location context and skin tone are hard requirements; make hair colour plainly visible at thumbnail size and do not reinterpret or diversify away from it
|
||||||
|
- for character images, vary face shape, nose, eyes, mouth, jaw, expression, hairstyle silhouette, and posture so reusable portraits do not look like siblings unless the specification explicitly asks for family resemblance
|
||||||
- no bright flat studio backdrop, no plain colour-block background, no stock-photo lighting, and no cartoon simplicity
|
- no bright flat studio backdrop, no plain colour-block background, no stock-photo lighting, and no cartoon simplicity
|
||||||
- avoid private manuscript details, real names, excerpts, or recognisable copyrighted characters
|
- avoid private manuscript details, real names, excerpts, or recognisable copyrighted characters
|
||||||
- avoid muddy olive and brown grading, heavy sepia tones, underexposure, excessive darkness, low-contrast lighting, flat colour, antique oil-painting texture, distressed canvas texture, rough brushwork, overly sombre default expressions, backgrounds that merge into the subject, and monochrome or near-monochrome results
|
- avoid muddy olive and brown grading, heavy sepia tones, underexposure, excessive darkness, low-contrast lighting, flat colour, antique oil-painting texture, distressed canvas texture, rough brushwork, overly sombre default expressions, backgrounds that merge into the subject, and monochrome or near-monochrome results
|
||||||
|
|||||||
@ -184,6 +184,13 @@ public static class StoryIntelligenceIllustrationCompatibility
|
|||||||
rejected.Add($"presentation hard mismatch: wanted {evidence.Presentation}, candidate {candidate.Presentation}");
|
rejected.Add($"presentation hard mismatch: wanted {evidence.Presentation}, candidate {candidate.Presentation}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(evidence.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(candidate.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& isNamedCharacter)
|
||||||
|
{
|
||||||
|
rejected.Add($"named character with {evidence.HairColour} hair needs hair-specific artwork");
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.Equals(evidence.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase)
|
if (!string.Equals(evidence.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(candidate.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(candidate.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||||
&& KnownMismatch(evidence.HairColour, candidate.HairColour))
|
&& KnownMismatch(evidence.HairColour, candidate.HairColour))
|
||||||
|
|||||||
@ -996,8 +996,8 @@ public sealed class StoryMemoryService(
|
|||||||
Code = code,
|
Code = code,
|
||||||
Title = $"{age} {presentation} character",
|
Title = $"{age} {presentation} character",
|
||||||
Description = "Reusable character portrait generated from durable Story Memory appearance evidence. Not canonical to a specific manuscript.",
|
Description = "Reusable character portrait generated from durable Story Memory appearance evidence. Not canonical to a specific manuscript.",
|
||||||
VisualSubject = $"A {agePrompt} {presentation.ToLowerInvariant()} person from {era} {location} with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair. Skin tone requirement: {skinInstruction}.",
|
VisualSubject = $"A {agePrompt} {presentation.ToLowerInvariant()} person from {era} {location} with unmistakable {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair. Skin tone requirement: {skinInstruction}. Give this person a distinct face shape, nose, eyes, jaw, expression, and hairstyle silhouette so they do not resemble other reusable portraits.",
|
||||||
Composition = "single head-and-shoulders portrait with clear facial lighting, neutral story background, and strong thumbnail readability",
|
Composition = "single head-and-shoulders portrait with clear facial lighting, neutral story background, strong thumbnail readability, and visibly individual facial features",
|
||||||
Mood = "present, natural, story-aware",
|
Mood = "present, natural, story-aware",
|
||||||
ApparentAgeBand = age,
|
ApparentAgeBand = age,
|
||||||
Presentation = presentation,
|
Presentation = presentation,
|
||||||
|
|||||||
@ -698,15 +698,15 @@ input:focus-visible {
|
|||||||
animation: drawLine 760ms cubic-bezier(.16, .84, .28, 1) forwards;
|
animation: drawLine 760ms cubic-bezier(.16, .84, .28, 1) forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-connection-group--presence .story-exp-connection,
|
.story-exp-connection-group--presence-cluster .story-exp-connection {
|
||||||
.story-exp-connection-group--presence-soft .story-exp-connection {
|
stroke: rgba(125, 193, 255, 0.62);
|
||||||
stroke: rgba(125, 193, 255, 0.52);
|
stroke-width: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-connection-group--presence-soft .story-exp-connection,
|
.story-exp-connection-group--presence-cluster .story-exp-connection,
|
||||||
.story-exp-connection-group--knowledge .story-exp-connection,
|
.story-exp-connection-group--knowledge .story-exp-connection,
|
||||||
.story-exp-connection-group--asset-link .story-exp-connection {
|
.story-exp-connection-group--asset-link .story-exp-connection {
|
||||||
stroke-dasharray: 0.16 0.12;
|
stroke-dasharray: 0.08 0.13;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-connection-group--support .story-exp-connection {
|
.story-exp-connection-group--support .story-exp-connection {
|
||||||
@ -714,15 +714,19 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-connection-group--conflict .story-exp-connection {
|
.story-exp-connection-group--conflict .story-exp-connection {
|
||||||
stroke: rgba(227, 140, 168, 0.7);
|
stroke: rgba(227, 140, 168, 0.42);
|
||||||
stroke-dasharray: 0.2 0.12;
|
stroke-dasharray: 0.2 0.12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-connection-group--uncertain .story-exp-connection {
|
.story-exp-connection-group--uncertain .story-exp-connection {
|
||||||
stroke: rgba(182, 162, 255, 0.48);
|
stroke: rgba(182, 162, 255, 0.32);
|
||||||
stroke-dasharray: 0.12 0.11;
|
stroke-dasharray: 0.12 0.11;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-exp-connection-group--strong .story-exp-connection {
|
||||||
|
stroke: rgba(215, 168, 93, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
.story-exp-connection-group--knowledge .story-exp-connection {
|
.story-exp-connection-group--knowledge .story-exp-connection {
|
||||||
stroke: rgba(182, 162, 255, 0.44);
|
stroke: rgba(182, 162, 255, 0.44);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -682,7 +682,10 @@
|
|||||||
const activeIds = new Set();
|
const activeIds = new Set();
|
||||||
const occupiedRects = layoutRects(stageRect);
|
const occupiedRects = layoutRects(stageRect);
|
||||||
|
|
||||||
scene.relationships.forEach((relationship, index) => {
|
scene.relationships
|
||||||
|
.filter((relationship) => relationship.weight >= 72)
|
||||||
|
.slice(0, 2)
|
||||||
|
.forEach((relationship, index) => {
|
||||||
const source = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.sourceId)}"]`);
|
const source = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.sourceId)}"]`);
|
||||||
const target = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.targetId)}"]`);
|
const target = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.targetId)}"]`);
|
||||||
if (!source || !target) return;
|
if (!source || !target) return;
|
||||||
@ -692,29 +695,22 @@
|
|||||||
const obstacles = connectionObstacles(stageRect, source, target);
|
const obstacles = connectionObstacles(stageRect, source, target);
|
||||||
const id = relationship.id || `relationship-${relationship.sourceId}-${relationship.targetId}`;
|
const id = relationship.id || `relationship-${relationship.sourceId}-${relationship.targetId}`;
|
||||||
activeIds.add(id);
|
activeIds.add(id);
|
||||||
drawn.push(drawConnection(id, start, end, relationshipTone(relationship), relationship.weight, index < 2 ? inlineRelationshipLabel(relationship) : "", index, obstacles, occupiedRects));
|
drawn.push(drawConnection(id, start, end, relationshipTone(relationship), relationship.weight, "", index, obstacles, occupiedRects));
|
||||||
});
|
});
|
||||||
|
|
||||||
const pov = dom.visualStage.querySelector(`[data-character-id="${cssEscape(scene.povCharacterId)}"]`);
|
|
||||||
const location = dom.visualStage.querySelector("[data-location-id]");
|
const location = dom.visualStage.querySelector("[data-location-id]");
|
||||||
if (pov && location) {
|
const presentCharacters = scene.characters
|
||||||
const id = `presence-${scene.povCharacterId}-${location.dataset.locationId}`;
|
.filter((character) => !character.status || !character.status.toLowerCase().includes("absent"))
|
||||||
|
.map((character) => dom.visualStage.querySelector(`[data-character-id="${cssEscape(character.id)}"]`))
|
||||||
|
.filter(Boolean);
|
||||||
|
const cluster = characterClusterPoint(presentCharacters, stageRect);
|
||||||
|
if (cluster && location) {
|
||||||
|
const id = `presence-cluster-${location.dataset.locationId}`;
|
||||||
activeIds.add(id);
|
activeIds.add(id);
|
||||||
drawn.push(drawConnection(id, connectionPoint(pov, stageRect, location), connectionPoint(location, stageRect, pov), "presence", 68, "", drawn.length, connectionObstacles(stageRect, pov, location), occupiedRects));
|
const locationPoint = connectionPoint(location, stageRect, { getBoundingClientRect: () => ({ left: stageRect.left + cluster.x, top: stageRect.top + cluster.y, width: 0, height: 0 }) });
|
||||||
|
drawn.push(drawConnection(id, cluster, locationPoint, "presence-cluster", 54, `${presentCharacters.length} here`, drawn.length, [], occupiedRects));
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.characters
|
|
||||||
.filter((character) => character.id !== scene.povCharacterId && character.weight >= 65)
|
|
||||||
.slice(0, 1)
|
|
||||||
.forEach((character) => {
|
|
||||||
const node = dom.visualStage.querySelector(`[data-character-id="${cssEscape(character.id)}"]`);
|
|
||||||
if (node && location) {
|
|
||||||
const id = `presence-soft-${character.id}-${location.dataset.locationId}`;
|
|
||||||
activeIds.add(id);
|
|
||||||
drawn.push(drawConnection(id, connectionPoint(node, stageRect, location), connectionPoint(location, stageRect, node), "presence-soft", character.weight, "", drawn.length, connectionObstacles(stageRect, node, location), occupiedRects));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
removeInactiveSvgConnections(activeIds);
|
removeInactiveSvgConnections(activeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -737,7 +733,7 @@
|
|||||||
path.setAttribute("class", "story-exp-connection");
|
path.setAttribute("class", "story-exp-connection");
|
||||||
group.append(path);
|
group.append(path);
|
||||||
}
|
}
|
||||||
const route = routeConnection(start, end, index, obstacles);
|
const route = routeConnection(start, end, index, obstacles, tone);
|
||||||
const d = `M ${start.x} ${start.y} C ${route.startControl.x} ${route.startControl.y}, ${route.endControl.x} ${route.endControl.y}, ${end.x} ${end.y}`;
|
const d = `M ${start.x} ${start.y} C ${route.startControl.x} ${route.startControl.y}, ${route.endControl.x} ${route.endControl.y}, ${end.x} ${end.y}`;
|
||||||
path.setAttribute("d", d);
|
path.setAttribute("d", d);
|
||||||
path.style.strokeWidth = String(0.9 + Math.min(weight, 100) / 62);
|
path.style.strokeWidth = String(0.9 + Math.min(weight, 100) / 62);
|
||||||
@ -808,10 +804,10 @@
|
|||||||
const others = characters
|
const others = characters
|
||||||
.filter((character) => character.id !== pov?.id)
|
.filter((character) => character.id !== pov?.id)
|
||||||
.sort((a, b) => b.weight - a.weight);
|
.sort((a, b) => b.weight - a.weight);
|
||||||
const povSize = Math.round(Math.min(188, Math.max(150, width * 0.145)));
|
const povSize = Math.round(Math.min(188, Math.max(150, width * 0.142)));
|
||||||
if (pov) {
|
if (pov) {
|
||||||
positions.set(pov.id, {
|
positions.set(pov.id, {
|
||||||
x: clamp(Math.round(width * 0.25 - povSize / 2), 28, width * 0.52 - povSize),
|
x: clamp(Math.round(width * 0.28 - povSize / 2), 28, width * 0.58 - povSize),
|
||||||
y: clamp(Math.round(height * 0.28 - povSize / 2), 54, height * 0.54 - povSize),
|
y: clamp(Math.round(height * 0.28 - povSize / 2), 54, height * 0.54 - povSize),
|
||||||
size: povSize,
|
size: povSize,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
@ -823,13 +819,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const slots = [
|
const slots = [
|
||||||
{ x: 0.48, y: 0.46, role: "partner" },
|
{ x: 0.55, y: 0.46, role: "partner" },
|
||||||
{ x: 0.12, y: 0.58, role: "support" },
|
{ x: 0.14, y: 0.58, role: "support" },
|
||||||
{ x: 0.12, y: 0.18, role: "support" },
|
{ x: 0.12, y: 0.18, role: "support" },
|
||||||
{ x: 0.36, y: 0.72, role: "context" },
|
{ x: 0.42, y: 0.72, role: "context" },
|
||||||
{ x: 0.28, y: 0.46, role: "context" },
|
{ x: 0.34, y: 0.46, role: "context" },
|
||||||
{ x: 0.52, y: 0.66, role: "context" },
|
{ x: 0.60, y: 0.66, role: "context" },
|
||||||
{ x: 0.06, y: 0.38, role: "context" }
|
{ x: 0.06, y: 0.38, role: "context" },
|
||||||
|
{ x: 0.24, y: 0.76, role: "context" }
|
||||||
];
|
];
|
||||||
const placed = [...positions.values()];
|
const placed = [...positions.values()];
|
||||||
others.forEach((character, index) => {
|
others.forEach((character, index) => {
|
||||||
@ -974,14 +971,28 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeConnection(start, end, index, obstacles) {
|
function characterClusterPoint(nodes, parentRect) {
|
||||||
|
if (!nodes.length) return null;
|
||||||
|
const rects = nodes
|
||||||
|
.map((node) => node.querySelector(".story-character__image") || node)
|
||||||
|
.map((node) => node.getBoundingClientRect());
|
||||||
|
const right = Math.max(...rects.map((rect) => rect.right)) - parentRect.left;
|
||||||
|
const top = Math.min(...rects.map((rect) => rect.top)) - parentRect.top;
|
||||||
|
const bottom = Math.max(...rects.map((rect) => rect.bottom)) - parentRect.top;
|
||||||
|
return {
|
||||||
|
x: right + 18,
|
||||||
|
y: (top + bottom) / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeConnection(start, end, index, obstacles, tone) {
|
||||||
const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
||||||
const dx = end.x - start.x;
|
const dx = end.x - start.x;
|
||||||
const dy = end.y - start.y;
|
const dy = end.y - start.y;
|
||||||
const length = Math.max(1, Math.hypot(dx, dy));
|
const length = Math.max(1, Math.hypot(dx, dy));
|
||||||
const normal = { x: -dy / length, y: dx / length };
|
const normal = { x: -dy / length, y: dx / length };
|
||||||
const tangent = { x: dx / length, y: dy / length };
|
const tangent = { x: dx / length, y: dy / length };
|
||||||
const base = Math.max(34, Math.min(122, length * 0.2));
|
const base = tone === "presence-cluster" ? Math.max(18, Math.min(46, length * 0.08)) : Math.max(22, Math.min(68, length * 0.12));
|
||||||
const direction = index % 2 === 0 ? 1 : -1;
|
const direction = index % 2 === 0 ? 1 : -1;
|
||||||
const offsets = [direction * base, -direction * base, direction * base * 1.45, -direction * base * 1.45, 0];
|
const offsets = [direction * base, -direction * base, direction * base * 1.45, -direction * base * 1.45, 0];
|
||||||
let best = offsets[0];
|
let best = offsets[0];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user