@forgeax/engine-render 0.1.6 → 0.1.7
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/README.md +139 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +1 -1
- package/dist/assembly/factory.d.ts +7 -0
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/authoring.d.ts +1 -0
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-QC4VW3XJ.mjs → chunk-HCOGPHKS.mjs} +17 -3
- package/dist/chunk-HCOGPHKS.mjs.map +1 -0
- package/dist/chunk-YEJIUW7D.mjs +1152 -0
- package/dist/chunk-YEJIUW7D.mjs.map +1 -0
- package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +2 -0
- package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +2 -0
- package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/lines.d.ts +5 -0
- package/dist/components/lines.d.ts.map +1 -0
- package/dist/components/points.d.ts +15 -0
- package/dist/components/points.d.ts.map +1 -0
- package/dist/construct-renderer.mjs +952 -497
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/errors/render.d.ts +82 -1
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/features/render-graph-raster.d.ts +5 -1
- package/dist/features/render-graph-raster.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +3 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +2 -0
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/points-lines/__tests__/bounds.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/inspection.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/points-lines.bench.d.ts +2 -0
- package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +1 -0
- package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +1 -0
- package/dist/points-lines/admission.d.ts +36 -0
- package/dist/points-lines/admission.d.ts.map +1 -0
- package/dist/points-lines/bounds.d.ts +8 -0
- package/dist/points-lines/bounds.d.ts.map +1 -0
- package/dist/points-lines/expansion-cache.d.ts +28 -0
- package/dist/points-lines/expansion-cache.d.ts.map +1 -0
- package/dist/points-lines/inspection.d.ts +53 -0
- package/dist/points-lines/inspection.d.ts.map +1 -0
- package/dist/points-lines/prepare.d.ts +67 -0
- package/dist/points-lines/prepare.d.ts.map +1 -0
- package/dist/points-lines/record.d.ts +60 -0
- package/dist/points-lines/record.d.ts.map +1 -0
- package/dist/points-lines/snapshot.d.ts +49 -0
- package/dist/points-lines/snapshot.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame.d.ts +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +7 -0
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +17 -0
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +6 -0
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/render-contract.d.ts +10 -2
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +3 -0
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +4 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +3 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +7 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/package.json +19 -19
- package/src/__tests__/authoring-path.unit.test.ts +9 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +12 -2
- package/src/__tests__/material-cooked-projection.unit.test.ts +29 -7
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +28 -28
- package/src/__tests__/render-error-code-owner.test-d.ts +6 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +19 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +38 -0
- package/src/assembly/factory.ts +72 -3
- package/src/assembly/material/assembly.ts +2 -5
- package/src/authoring.ts +1 -0
- package/src/components/__tests__/points-lines-admission.unit.test.ts +219 -0
- package/src/components/__tests__/points-lines-schema.unit.test.ts +56 -0
- package/src/components/index.ts +7 -0
- package/src/components/lines.ts +18 -0
- package/src/components/points.ts +40 -0
- package/src/errors/render.ts +144 -1
- package/src/features/render-graph-raster.ts +22 -3
- package/src/gpu-driven/production-raster.ts +6 -1
- package/src/index.ts +17 -0
- package/src/inspection-types.ts +4 -0
- package/src/pbr-pipeline.ts +8 -1
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/bounds.unit.test.ts +21 -0
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +157 -0
- package/src/points-lines/__tests__/inspection-lanes.integration.test.ts +146 -0
- package/src/points-lines/__tests__/inspection.unit.test.ts +173 -0
- package/src/points-lines/__tests__/lane-structural.integration.test.ts +122 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +400 -0
- package/src/points-lines/__tests__/raster-lane.integration.test.ts +44 -0
- package/src/points-lines/__tests__/snapshot.unit.test.ts +101 -0
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +73 -0
- package/src/points-lines/admission.ts +238 -0
- package/src/points-lines/bounds.ts +23 -0
- package/src/points-lines/expansion-cache.ts +215 -0
- package/src/points-lines/inspection.ts +97 -0
- package/src/points-lines/prepare.ts +177 -0
- package/src/points-lines/record.ts +125 -0
- package/src/points-lines/snapshot.ts +130 -0
- package/src/record/frame-lighting.ts +14 -2
- package/src/record/frame.ts +12 -1
- package/src/record/main-pass-geometry.ts +92 -0
- package/src/record/main-pass-sprite-draws.ts +1 -1
- package/src/record/main-pass.ts +6 -3
- package/src/record/render-context.ts +20 -0
- package/src/record/shadow-pass.ts +16 -5
- package/src/record/skybox-post-pass.ts +2 -1
- package/src/record/typed-frame-graph.ts +17 -16
- package/src/record/view-ubo.ts +47 -0
- package/src/render-contract.ts +13 -2
- package/src/render-system-extract.ts +71 -8
- package/src/render-system.ts +304 -2
- package/src/scene/render-scene-types.ts +3 -0
- package/src/scene/render-scene.ts +57 -2
- package/dist/chunk-QC4VW3XJ.mjs.map +0 -1
- package/dist/chunk-SCRSAK5B.mjs +0 -480
- package/dist/chunk-SCRSAK5B.mjs.map +0 -1
package/src/assembly/factory.ts
CHANGED
|
@@ -153,9 +153,10 @@ import {
|
|
|
153
153
|
type PipelineSpec,
|
|
154
154
|
PipelineSpecError,
|
|
155
155
|
} from '../pipeline-spec';
|
|
156
|
+
import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
|
|
156
157
|
import type { FrameObservationOptions } from '../record/frame';
|
|
157
158
|
import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
|
|
158
|
-
import { VIEW_UNIFORM_BUFFER_SIZE } from '../record/view-ubo';
|
|
159
|
+
import { POINTS_LINES_VIEW_BUFFER_SIZE, VIEW_UNIFORM_BUFFER_SIZE } from '../record/view-ubo';
|
|
159
160
|
import type {
|
|
160
161
|
DrawOwnerOptions,
|
|
161
162
|
FrameObservationRequest,
|
|
@@ -501,7 +502,7 @@ export function selectNoColorPbrVariant(
|
|
|
501
502
|
variantSet: string | undefined,
|
|
502
503
|
): MaterialShaderManifestEntry['variants'][number] | undefined {
|
|
503
504
|
const hdrpRequested =
|
|
504
|
-
variantSet === '' || variantSet?.includes('CLUSTER_FORWARD_AVAILABLE=true')
|
|
505
|
+
variantSet === '' || (variantSet?.includes('CLUSTER_FORWARD_AVAILABLE=true') ?? false);
|
|
505
506
|
return manifestEntry?.variants.find(
|
|
506
507
|
(variant) =>
|
|
507
508
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
@@ -694,6 +695,21 @@ export function resolveMaterialShaderVariantSet(
|
|
|
694
695
|
: sorted.map(([name, value]) => `${name}=${value}`).join('+');
|
|
695
696
|
}
|
|
696
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Keep a single-source material on its canonical module identity. A manifest
|
|
700
|
+
* entry with no variants cannot consume a synthesized capability or geometry
|
|
701
|
+
* axis, so carrying one into the pipeline cache only creates an uncached lazy
|
|
702
|
+
* compile beside the prewarmed source module.
|
|
703
|
+
*/
|
|
704
|
+
export function normalizeMaterialShaderVariantSet(
|
|
705
|
+
requestedVariantSet: string | undefined,
|
|
706
|
+
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
707
|
+
): string | undefined {
|
|
708
|
+
return manifestEntry !== undefined && manifestEntry.variants.length === 0
|
|
709
|
+
? undefined
|
|
710
|
+
: requestedVariantSet;
|
|
711
|
+
}
|
|
712
|
+
|
|
697
713
|
export async function createRenderer(
|
|
698
714
|
canvas: unknown,
|
|
699
715
|
options?: RendererOptions,
|
|
@@ -2073,7 +2089,9 @@ async function makeWebGPURenderer(
|
|
|
2073
2089
|
// canonical HDRP variant key and would pair an HDRP PSO with the URP mesh
|
|
2074
2090
|
// bind group. Builtin forgeax:: shaders keep the compatibility request
|
|
2075
2091
|
// when a reduced test/legacy manifest omits variant metadata.
|
|
2076
|
-
|
|
2092
|
+
// A single-source manifest entry must stay on its canonical module key;
|
|
2093
|
+
// otherwise a synthesized capability axis races the prewarmed module.
|
|
2094
|
+
let effectiveVariantSet = normalizeMaterialShaderVariantSet(resolvedVariantSet, manifestEntry);
|
|
2077
2095
|
if (
|
|
2078
2096
|
vertexLayoutProjection === undefined &&
|
|
2079
2097
|
manifestEntry?.variants.some((variant) => 'VERTEX_COLOR_AVAILABLE' in variant.defines)
|
|
@@ -2590,6 +2608,7 @@ async function makeWebGPURenderer(
|
|
|
2590
2608
|
features: Object.freeze(
|
|
2591
2609
|
(internals.featureHost?.features ?? []).map((feature) => feature.identity),
|
|
2592
2610
|
),
|
|
2611
|
+
featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
|
|
2593
2612
|
frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
|
|
2594
2613
|
visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
|
|
2595
2614
|
renderScene: renderSystem.renderScene,
|
|
@@ -3569,6 +3588,13 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3569
3588
|
draw: (request) => {
|
|
3570
3589
|
const result = renderer.drawFrame(request);
|
|
3571
3590
|
if (!result.ok) return err(drawError(result.error));
|
|
3591
|
+
emit(
|
|
3592
|
+
Object.freeze({
|
|
3593
|
+
kind: 'frame-submitted',
|
|
3594
|
+
frameId: result.value.frameId,
|
|
3595
|
+
deviceGeneration: result.value.deviceGeneration,
|
|
3596
|
+
}),
|
|
3597
|
+
);
|
|
3572
3598
|
return ok(result.value);
|
|
3573
3599
|
},
|
|
3574
3600
|
inspect: () => renderer.inspect(),
|
|
@@ -5082,6 +5108,34 @@ async function buildReadyWebGPU(
|
|
|
5082
5108
|
brdfLut: iblBrdfLutEntry.wgsl,
|
|
5083
5109
|
});
|
|
5084
5110
|
}
|
|
5111
|
+
// Points/Lines are an engine-owned record path rather than a producer
|
|
5112
|
+
// RenderFeature, so they cannot declare `requiredMaterialShaders`. Their
|
|
5113
|
+
// material pipeline still goes through the same lazy shader-module adapter
|
|
5114
|
+
// as producer features. Prewarm the optional manifest artifact here so a
|
|
5115
|
+
// first frame cannot race async module compilation and silently skip its
|
|
5116
|
+
// draw while the adapter is still pending. Older/minimal manifests may not
|
|
5117
|
+
// carry the optional entry; those hosts retain their existing behavior.
|
|
5118
|
+
const pointsLinesLookup = registry.findMaterialArtifact(POINTS_LINES_MATERIAL_SHADER_ID);
|
|
5119
|
+
if (pointsLinesLookup.ok) {
|
|
5120
|
+
const pointsLinesLabel = `module-${POINTS_LINES_MATERIAL_SHADER_ID}`;
|
|
5121
|
+
const pointsLinesShaderResult = await runShimStep(
|
|
5122
|
+
() =>
|
|
5123
|
+
asyncCreateShaderModule
|
|
5124
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
5125
|
+
code: pointsLinesLookup.value.source,
|
|
5126
|
+
label: pointsLinesLabel,
|
|
5127
|
+
})
|
|
5128
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
5129
|
+
code: pointsLinesLookup.value.source,
|
|
5130
|
+
label: pointsLinesLabel,
|
|
5131
|
+
}),
|
|
5132
|
+
'shader-compile-failed',
|
|
5133
|
+
`engine material shader '${POINTS_LINES_MATERIAL_SHADER_ID}' compiled`,
|
|
5134
|
+
`inspect the composed WGSL for '${POINTS_LINES_MATERIAL_SHADER_ID}' and check device.features`,
|
|
5135
|
+
);
|
|
5136
|
+
if (!pointsLinesShaderResult.ok) throw pointsLinesShaderResult.error;
|
|
5137
|
+
seedShaderModule(pointsLinesLabel, pointsLinesShaderResult.value);
|
|
5138
|
+
}
|
|
5085
5139
|
// Producer-declared material shader modules use the same source and module
|
|
5086
5140
|
// label as getMaterialShaderPipeline. Prewarming them here keeps the
|
|
5087
5141
|
// renderer.initialization barrier meaningful for prepared graphics: the first draw
|
|
@@ -5899,6 +5953,20 @@ async function buildReadyWebGPU(
|
|
|
5899
5953
|
);
|
|
5900
5954
|
if (!viewUboResult.ok) throw viewUboResult.error;
|
|
5901
5955
|
|
|
5956
|
+
const pointsLinesViewBufferResult = runShimSyncStep(
|
|
5957
|
+
() =>
|
|
5958
|
+
rhiDevice.createBuffer({
|
|
5959
|
+
label: 'points-lines-view-ubo',
|
|
5960
|
+
size: POINTS_LINES_VIEW_BUFFER_SIZE,
|
|
5961
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
5962
|
+
mappedAtCreation: false,
|
|
5963
|
+
}),
|
|
5964
|
+
'webgpu-runtime-error',
|
|
5965
|
+
'createBuffer (points-lines view ubo) succeeded',
|
|
5966
|
+
'check device.limits.maxUniformBufferBindingSize',
|
|
5967
|
+
);
|
|
5968
|
+
if (!pointsLinesViewBufferResult.ok) throw pointsLinesViewBufferResult.error;
|
|
5969
|
+
|
|
5902
5970
|
// feat-20260613-csm-cascaded-shadow-maps M5 / w28: per-pass cascade-index
|
|
5903
5971
|
// UBO consumed by shadow_caster.wgsl. Stable singleton; the record stage
|
|
5904
5972
|
// queue.writeBuffer-overwrites the fields immediately before each pass's
|
|
@@ -7817,6 +7885,7 @@ async function buildReadyWebGPU(
|
|
|
7817
7885
|
materialBindGroupLayout: materialBglResult.value,
|
|
7818
7886
|
meshBindGroupLayout: meshArrayBglResult.value,
|
|
7819
7887
|
viewUniformBuffer: viewUboResult.value,
|
|
7888
|
+
pointsLinesViewBuffer: pointsLinesViewBufferResult.value,
|
|
7820
7889
|
shadowCasterCascadeBuffer: shadowCasterCascadeUboResult.value,
|
|
7821
7890
|
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
|
|
7822
7891
|
// T-M2-05: PipelineState now references the wrapper objects owned by
|
|
@@ -39,13 +39,10 @@ function projectPass(
|
|
|
39
39
|
export function assembleMaterialProjection(record: CookedMaterialRecord): MaterialRenderProjection {
|
|
40
40
|
return {
|
|
41
41
|
materialGuid: record.guid,
|
|
42
|
-
specializationKey: record.receipt.
|
|
42
|
+
specializationKey: record.specializationKey ?? record.receipt.identity.programIdentity,
|
|
43
43
|
artifactHash: record.artifact.digest,
|
|
44
44
|
passes: record.resolved.passes.map((pass) => projectPass(record, pass)),
|
|
45
45
|
runtimeValues: record.resolved.values,
|
|
46
|
-
staticSelection:
|
|
47
|
-
.filter((parameter) => parameter.static)
|
|
48
|
-
.map((parameter) => parameter.name)
|
|
49
|
-
.sort(),
|
|
46
|
+
staticSelection: [],
|
|
50
47
|
};
|
|
51
48
|
}
|
package/src/authoring.ts
CHANGED
|
@@ -13,4 +13,5 @@ export {
|
|
|
13
13
|
type FullscreenRenderFeatureOptions,
|
|
14
14
|
} from './features/fullscreen';
|
|
15
15
|
export { SPRITE_PREMULTIPLIED_ALPHA_BLEND } from './materials';
|
|
16
|
+
export { setActiveCamera } from './systems/active-camera';
|
|
16
17
|
export { TransparentSort } from './systems/transparent-sort-config';
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { MeshAsset, PrimitiveTopology } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
PointsLinesBudgetExceededError,
|
|
5
|
+
PointsLinesInvalidStyleError,
|
|
6
|
+
PointsLinesMaterialUnsupportedError,
|
|
7
|
+
PointsLinesPrepareFailedError,
|
|
8
|
+
PointsLinesStyleUnsupportedError,
|
|
9
|
+
PointsLinesTopologyMismatchError,
|
|
10
|
+
} from '../../errors/render';
|
|
11
|
+
import { Materials } from '../../materials';
|
|
12
|
+
import { admitPointsLines, type PointsLinesAdmissionInput } from '../../points-lines/admission';
|
|
13
|
+
|
|
14
|
+
const unlitMaterial = Materials.unlit([1, 0.5, 0.25, 1], { castShadow: false });
|
|
15
|
+
|
|
16
|
+
function mesh(topology: PrimitiveTopology, count = 4, indexed = false): MeshAsset {
|
|
17
|
+
const vertices = new Float32Array(count * 3);
|
|
18
|
+
return {
|
|
19
|
+
kind: 'mesh',
|
|
20
|
+
vertices,
|
|
21
|
+
...(indexed ? { indices: new Uint16Array(count) } : {}),
|
|
22
|
+
attributes: { position: vertices },
|
|
23
|
+
aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
|
|
24
|
+
submeshes: [
|
|
25
|
+
{
|
|
26
|
+
indexOffset: 0,
|
|
27
|
+
indexCount: indexed ? count : 0,
|
|
28
|
+
vertexCount: count,
|
|
29
|
+
topology,
|
|
30
|
+
materialSlot: 0,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
materialSlots: [{ slotName: 'default' }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pointsInput(
|
|
38
|
+
overrides: Partial<PointsLinesAdmissionInput> = {},
|
|
39
|
+
): PointsLinesAdmissionInput {
|
|
40
|
+
return {
|
|
41
|
+
entity: 7,
|
|
42
|
+
points: { sizePx: 4, shape: 0 },
|
|
43
|
+
mesh: mesh('point-list', 3),
|
|
44
|
+
material: unlitMaterial,
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function linesInput(overrides: Partial<PointsLinesAdmissionInput> = {}): PointsLinesAdmissionInput {
|
|
50
|
+
return {
|
|
51
|
+
entity: 8,
|
|
52
|
+
lines: { widthPx: 1 },
|
|
53
|
+
mesh: mesh('line-list', 4),
|
|
54
|
+
material: unlitMaterial,
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function expectRefusal(
|
|
60
|
+
result: ReturnType<typeof admitPointsLines>,
|
|
61
|
+
code: string,
|
|
62
|
+
detail: Record<string, unknown>,
|
|
63
|
+
): void {
|
|
64
|
+
expect(result.ok).toBe(false);
|
|
65
|
+
if (result.ok) return;
|
|
66
|
+
if (!('detail' in result.error)) throw new Error('expected a detailed Points/Lines refusal');
|
|
67
|
+
expect(result.error.code).toBe(code);
|
|
68
|
+
expect(result.error.expected).toEqual(expect.any(String));
|
|
69
|
+
expect(result.error.hint).toEqual(expect.any(String));
|
|
70
|
+
expect(result.error.detail).toMatchObject(detail);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('Points and Lines admission', () => {
|
|
74
|
+
it('accepts indexed and non-indexed point and line-list geometry', () => {
|
|
75
|
+
const indexedPoints = admitPointsLines(pointsInput({ mesh: mesh('point-list', 3, true) }));
|
|
76
|
+
const nonIndexedLines = admitPointsLines(linesInput());
|
|
77
|
+
|
|
78
|
+
expect(indexedPoints.ok).toBe(true);
|
|
79
|
+
expect(nonIndexedLines.ok).toBe(true);
|
|
80
|
+
if (indexedPoints.ok) expect(indexedPoints.value.pointCount).toBe(3);
|
|
81
|
+
if (nonIndexedLines.ok) expect(nonIndexedLines.value.segmentCount).toBe(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects component conflicts and invalid numeric styles before topology', () => {
|
|
85
|
+
const conflict = admitPointsLines(pointsInput({ lines: { widthPx: 1 } }));
|
|
86
|
+
const invalidSize = admitPointsLines(pointsInput({ points: { sizePx: Number.NaN, shape: 0 } }));
|
|
87
|
+
const invalidWidth = admitPointsLines(linesInput({ lines: { widthPx: 0 } }));
|
|
88
|
+
|
|
89
|
+
expectRefusal(conflict, 'points-lines-invalid-style', {
|
|
90
|
+
entity: 7,
|
|
91
|
+
component: 'Points/Lines',
|
|
92
|
+
field: 'components',
|
|
93
|
+
});
|
|
94
|
+
expectRefusal(invalidSize, 'points-lines-invalid-style', {
|
|
95
|
+
entity: 7,
|
|
96
|
+
component: 'Points',
|
|
97
|
+
field: 'sizePx',
|
|
98
|
+
value: Number.NaN,
|
|
99
|
+
});
|
|
100
|
+
expectRefusal(invalidWidth, 'points-lines-invalid-style', {
|
|
101
|
+
entity: 8,
|
|
102
|
+
component: 'Lines',
|
|
103
|
+
field: 'widthPx',
|
|
104
|
+
value: 0,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('rejects unsupported topologies, odd line tails, and mixed candidates atomically', () => {
|
|
109
|
+
const strip = admitPointsLines(linesInput({ mesh: mesh('line-strip', 4) }));
|
|
110
|
+
const triangles = admitPointsLines(pointsInput({ mesh: mesh('triangle-list', 3) }));
|
|
111
|
+
const odd = admitPointsLines(linesInput({ mesh: mesh('line-list', 3) }));
|
|
112
|
+
const baseMixedMesh = mesh('line-list', 2);
|
|
113
|
+
const mixedMesh: MeshAsset = {
|
|
114
|
+
...baseMixedMesh,
|
|
115
|
+
submeshes: [
|
|
116
|
+
...baseMixedMesh.submeshes,
|
|
117
|
+
{
|
|
118
|
+
indexOffset: 0,
|
|
119
|
+
indexCount: 0,
|
|
120
|
+
vertexCount: 3,
|
|
121
|
+
topology: 'triangle-list',
|
|
122
|
+
materialSlot: 0,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
const mixed = admitPointsLines(linesInput({ mesh: mixedMesh }));
|
|
127
|
+
|
|
128
|
+
expectRefusal(strip, 'points-lines-style-unsupported', {
|
|
129
|
+
lane: 'admission',
|
|
130
|
+
field: 'topology',
|
|
131
|
+
member: 'line-strip',
|
|
132
|
+
});
|
|
133
|
+
expectRefusal(triangles, 'points-lines-topology-mismatch', {
|
|
134
|
+
entity: 7,
|
|
135
|
+
expected: 'point-list',
|
|
136
|
+
actual: 'triangle-list',
|
|
137
|
+
submesh: 0,
|
|
138
|
+
});
|
|
139
|
+
expectRefusal(odd, 'points-lines-topology-mismatch', {
|
|
140
|
+
entity: 8,
|
|
141
|
+
expected: 'line-list pairs',
|
|
142
|
+
actual: 'line-list odd tail',
|
|
143
|
+
submesh: 0,
|
|
144
|
+
});
|
|
145
|
+
expectRefusal(mixed, 'points-lines-topology-mismatch', {
|
|
146
|
+
entity: 8,
|
|
147
|
+
submesh: 1,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('rejects non-unlit materials and explicit resource budgets', () => {
|
|
152
|
+
const lit = Materials.standard({ baseColor: [1, 1, 1, 1] });
|
|
153
|
+
const material = admitPointsLines(linesInput({ material: lit }));
|
|
154
|
+
const budget = admitPointsLines(pointsInput({ limits: { maxPoints: 2 } }));
|
|
155
|
+
|
|
156
|
+
expectRefusal(material, 'points-lines-material-unsupported', {
|
|
157
|
+
entity: 8,
|
|
158
|
+
pass: 'deferred',
|
|
159
|
+
reason: expect.any(String),
|
|
160
|
+
});
|
|
161
|
+
expectRefusal(budget, 'points-lines-budget-exceeded', {
|
|
162
|
+
lane: 'admission',
|
|
163
|
+
requested: 3,
|
|
164
|
+
limit: 2,
|
|
165
|
+
unit: 'points',
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('keeps every closed refusal detail constructible and code-correlated', () => {
|
|
170
|
+
const errors = [
|
|
171
|
+
new PointsLinesInvalidStyleError({
|
|
172
|
+
entity: 1,
|
|
173
|
+
component: 'Points',
|
|
174
|
+
field: 'sizePx',
|
|
175
|
+
value: -1,
|
|
176
|
+
expected: 'finite sizePx > 0',
|
|
177
|
+
}),
|
|
178
|
+
new PointsLinesTopologyMismatchError({
|
|
179
|
+
entity: 1,
|
|
180
|
+
submesh: 0,
|
|
181
|
+
expected: 'point-list',
|
|
182
|
+
actual: 'triangle-list',
|
|
183
|
+
}),
|
|
184
|
+
new PointsLinesStyleUnsupportedError({
|
|
185
|
+
lane: 'admission',
|
|
186
|
+
field: 'topology',
|
|
187
|
+
member: 'line-strip',
|
|
188
|
+
supported: ['line-list'],
|
|
189
|
+
}),
|
|
190
|
+
new PointsLinesMaterialUnsupportedError({
|
|
191
|
+
entity: 1,
|
|
192
|
+
material: 'mat-1',
|
|
193
|
+
pass: 'forward',
|
|
194
|
+
module: 'forgeax_material::standard',
|
|
195
|
+
reason: 'surface lighting is not supported',
|
|
196
|
+
}),
|
|
197
|
+
new PointsLinesBudgetExceededError({
|
|
198
|
+
lane: 'admission',
|
|
199
|
+
requested: 5,
|
|
200
|
+
limit: 4,
|
|
201
|
+
unit: 'points',
|
|
202
|
+
}),
|
|
203
|
+
new PointsLinesPrepareFailedError({
|
|
204
|
+
owner: 'points-lines',
|
|
205
|
+
generation: 2,
|
|
206
|
+
stage: 'prepare',
|
|
207
|
+
cause: 'candidate validation failed',
|
|
208
|
+
lastKnownGood: false,
|
|
209
|
+
}),
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (const error of errors) {
|
|
213
|
+
expect(error.code).toMatch(/^points-lines-/);
|
|
214
|
+
expect(error.expected).toEqual(expect.any(String));
|
|
215
|
+
expect(error.hint).toEqual(expect.any(String));
|
|
216
|
+
expect(error.detail).toBeDefined();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { PointShape } from '@forgeax/engine-render';
|
|
3
|
+
import { Lines, PointShapeValue, Points, pointShapeFromU32 } from '@forgeax/engine-render';
|
|
4
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('Points and Lines public schema', () => {
|
|
7
|
+
it('creates both components through the public import and inserts them in World', () => {
|
|
8
|
+
const world = new World();
|
|
9
|
+
const pointEntity = world
|
|
10
|
+
.spawn({
|
|
11
|
+
component: Points,
|
|
12
|
+
data: { sizePx: 8, shape: PointShapeValue.circle },
|
|
13
|
+
})
|
|
14
|
+
.unwrap();
|
|
15
|
+
const lineEntity = world.spawn({ component: Lines, data: { widthPx: 4 } }).unwrap();
|
|
16
|
+
|
|
17
|
+
expect(world.get(pointEntity, Points).unwrap()).toMatchObject({
|
|
18
|
+
sizePx: 8,
|
|
19
|
+
shape: PointShapeValue.circle,
|
|
20
|
+
});
|
|
21
|
+
expect(world.get(lineEntity, Lines).unwrap()).toMatchObject({ widthPx: 4 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('keeps the frozen defaults and closed point shape decoder', () => {
|
|
25
|
+
const world = new World();
|
|
26
|
+
const pointEntity = world.spawn({ component: Points, data: {} }).unwrap();
|
|
27
|
+
const lineEntity = world.spawn({ component: Lines, data: {} }).unwrap();
|
|
28
|
+
|
|
29
|
+
expect(world.get(pointEntity, Points).unwrap()).toMatchObject({
|
|
30
|
+
sizePx: 4,
|
|
31
|
+
shape: PointShapeValue.square,
|
|
32
|
+
});
|
|
33
|
+
expect(world.get(lineEntity, Lines).unwrap().widthPx).toBe(1);
|
|
34
|
+
expect(Points.fields).toEqual({
|
|
35
|
+
sizePx: expect.objectContaining({ type: 'f32', default: 4 }),
|
|
36
|
+
shape: expect.objectContaining({
|
|
37
|
+
type: 'enum',
|
|
38
|
+
default: PointShapeValue.square,
|
|
39
|
+
labels: PointShapeValue,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
expect(Lines.fields).toEqual({
|
|
43
|
+
widthPx: expect.objectContaining({ type: 'f32', default: 1 }),
|
|
44
|
+
});
|
|
45
|
+
expect(pointShapeFromU32(PointShapeValue.square)).toBe('square');
|
|
46
|
+
expect(pointShapeFromU32(PointShapeValue.circle)).toBe('circle');
|
|
47
|
+
expect(pointShapeFromU32(99)).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('exposes only the three first-release style fields', () => {
|
|
51
|
+
type PublicPointShape = PointShape;
|
|
52
|
+
expectTypeOf<PublicPointShape>().toEqualTypeOf<'square' | 'circle'>();
|
|
53
|
+
expectTypeOf<keyof typeof Points.fields>().toEqualTypeOf<'sizePx' | 'shape'>();
|
|
54
|
+
expectTypeOf<keyof typeof Lines.fields>().toEqualTypeOf<'widthPx'>();
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -2,10 +2,17 @@ export * from './camera';
|
|
|
2
2
|
export * from './directional-light';
|
|
3
3
|
export { Instances, type InstancesData } from './instances';
|
|
4
4
|
export { Layer } from './layer';
|
|
5
|
+
export { Lines } from './lines';
|
|
5
6
|
export * from './mesh-filter';
|
|
6
7
|
export * from './mesh-renderer';
|
|
7
8
|
export { PointLight } from './point-light';
|
|
8
9
|
export { PointLightShadow } from './point-light-shadow';
|
|
10
|
+
export {
|
|
11
|
+
type PointShape,
|
|
12
|
+
PointShapeValue,
|
|
13
|
+
Points,
|
|
14
|
+
pointShapeFromU32,
|
|
15
|
+
} from './points';
|
|
9
16
|
export { PostProcessParams } from './post-process-params';
|
|
10
17
|
export {
|
|
11
18
|
SceneInstance,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineComponent } from '@forgeax/engine-ecs';
|
|
2
|
+
|
|
3
|
+
/** Screen-space independent line segment style. */
|
|
4
|
+
export const Lines = defineComponent(
|
|
5
|
+
'Lines',
|
|
6
|
+
{
|
|
7
|
+
widthPx: { type: 'f32', default: 1 },
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
meta: {
|
|
11
|
+
quickStart: 'Attach Lines to render line-list MeshAsset pairs in screen pixels.',
|
|
12
|
+
diagnostics: 'Inspect widthPx at the points-lines admission boundary.',
|
|
13
|
+
recovery: 'Use points-lines-invalid-style for non-finite or non-positive widthPx values.',
|
|
14
|
+
boundaries:
|
|
15
|
+
'Lines owns raster width only; MeshFilter and MeshRenderer own geometry and material.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineComponent } from '@forgeax/engine-ecs';
|
|
2
|
+
|
|
3
|
+
/** Public point raster shapes backed by the ECS enum column. */
|
|
4
|
+
export type PointShape = keyof typeof PointShapeValue;
|
|
5
|
+
|
|
6
|
+
/** Numeric labels stored by the public Points shape enum field. */
|
|
7
|
+
export const PointShapeValue = Object.freeze({
|
|
8
|
+
square: 0,
|
|
9
|
+
circle: 1,
|
|
10
|
+
} as const);
|
|
11
|
+
|
|
12
|
+
/** Decode the Points shape column without treating an unknown value as valid. */
|
|
13
|
+
export function pointShapeFromU32(value: number): PointShape | undefined {
|
|
14
|
+
switch (value) {
|
|
15
|
+
case PointShapeValue.square:
|
|
16
|
+
return 'square';
|
|
17
|
+
case PointShapeValue.circle:
|
|
18
|
+
return 'circle';
|
|
19
|
+
default:
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Screen-space point style. Geometry and material remain on their own owners. */
|
|
25
|
+
export const Points = defineComponent(
|
|
26
|
+
'Points',
|
|
27
|
+
{
|
|
28
|
+
sizePx: { type: 'f32', default: 4 },
|
|
29
|
+
shape: { type: 'enum', default: PointShapeValue.square, labels: PointShapeValue },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
meta: {
|
|
33
|
+
quickStart: 'Attach Points to render point-list MeshAsset vertices in screen pixels.',
|
|
34
|
+
diagnostics: 'Inspect sizePx and decode shape with pointShapeFromU32.',
|
|
35
|
+
recovery: 'Use points-lines-invalid-style for non-finite or non-positive sizePx values.',
|
|
36
|
+
boundaries:
|
|
37
|
+
'Points owns raster size and shape only; MeshFilter and MeshRenderer own geometry and material.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
);
|
package/src/errors/render.ts
CHANGED
|
@@ -1101,6 +1101,143 @@ export type RendererExpectedOperationError = {
|
|
|
1101
1101
|
readonly [Code in RendererOperationErrorCode]: RendererOperationError<Code>;
|
|
1102
1102
|
}[RendererOperationErrorCode];
|
|
1103
1103
|
|
|
1104
|
+
// -- Points/Lines admission errors -------------------------------------------
|
|
1105
|
+
|
|
1106
|
+
export interface PointsLinesInvalidStyleDetail {
|
|
1107
|
+
readonly entity: number;
|
|
1108
|
+
readonly component: 'Points' | 'Lines' | 'Points/Lines';
|
|
1109
|
+
readonly field: string;
|
|
1110
|
+
readonly value: number | string | undefined;
|
|
1111
|
+
readonly expected: string;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
export class PointsLinesInvalidStyleError extends Error {
|
|
1115
|
+
readonly code = 'points-lines-invalid-style' as const;
|
|
1116
|
+
readonly expected: string;
|
|
1117
|
+
readonly hint: string;
|
|
1118
|
+
readonly detail: PointsLinesInvalidStyleDetail;
|
|
1119
|
+
|
|
1120
|
+
constructor(detail: PointsLinesInvalidStyleDetail) {
|
|
1121
|
+
super(`invalid ${detail.component} style at ${detail.field}`);
|
|
1122
|
+
this.name = 'PointsLinesInvalidStyleError';
|
|
1123
|
+
this.expected = detail.expected;
|
|
1124
|
+
this.hint = `repair entity ${detail.entity} ${detail.component}.${detail.field} and retry admission`;
|
|
1125
|
+
this.detail = detail;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
export interface PointsLinesTopologyMismatchDetail {
|
|
1130
|
+
readonly entity: number;
|
|
1131
|
+
readonly submesh: number;
|
|
1132
|
+
readonly expected: string;
|
|
1133
|
+
readonly actual: string;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
export class PointsLinesTopologyMismatchError extends Error {
|
|
1137
|
+
readonly code = 'points-lines-topology-mismatch' as const;
|
|
1138
|
+
readonly expected: string;
|
|
1139
|
+
readonly hint: string;
|
|
1140
|
+
readonly detail: PointsLinesTopologyMismatchDetail;
|
|
1141
|
+
|
|
1142
|
+
constructor(detail: PointsLinesTopologyMismatchDetail) {
|
|
1143
|
+
super(`Points/Lines topology mismatch at submesh ${detail.submesh}`);
|
|
1144
|
+
this.name = 'PointsLinesTopologyMismatchError';
|
|
1145
|
+
this.expected = `submesh ${detail.submesh} topology is ${detail.expected}`;
|
|
1146
|
+
this.hint = `rebuild entity ${detail.entity} with ${detail.expected} geometry before retrying Points/Lines admission`;
|
|
1147
|
+
this.detail = detail;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
export interface PointsLinesStyleUnsupportedDetail {
|
|
1152
|
+
readonly lane: string;
|
|
1153
|
+
readonly field: string;
|
|
1154
|
+
readonly member: string;
|
|
1155
|
+
readonly supported: readonly string[];
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
export class PointsLinesStyleUnsupportedError extends Error {
|
|
1159
|
+
readonly code = 'points-lines-style-unsupported' as const;
|
|
1160
|
+
readonly expected: string;
|
|
1161
|
+
readonly hint: string;
|
|
1162
|
+
readonly detail: PointsLinesStyleUnsupportedDetail;
|
|
1163
|
+
|
|
1164
|
+
constructor(detail: PointsLinesStyleUnsupportedDetail) {
|
|
1165
|
+
super(`unsupported Points/Lines ${detail.field} member ${detail.member}`);
|
|
1166
|
+
this.name = 'PointsLinesStyleUnsupportedError';
|
|
1167
|
+
this.expected = `${detail.field} is one of ${detail.supported.join(', ')}`;
|
|
1168
|
+
this.hint = `use a supported Points/Lines ${detail.field} member or wait for a later milestone`;
|
|
1169
|
+
this.detail = detail;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export interface PointsLinesMaterialUnsupportedDetail {
|
|
1174
|
+
readonly entity: number;
|
|
1175
|
+
readonly material: string;
|
|
1176
|
+
readonly pass: string;
|
|
1177
|
+
readonly module: string;
|
|
1178
|
+
readonly reason: string;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
export class PointsLinesMaterialUnsupportedError extends Error {
|
|
1182
|
+
readonly code = 'points-lines-material-unsupported' as const;
|
|
1183
|
+
readonly expected = 'one engine-owned unlit forward MaterialAsset pass';
|
|
1184
|
+
readonly hint: string;
|
|
1185
|
+
readonly detail: PointsLinesMaterialUnsupportedDetail;
|
|
1186
|
+
|
|
1187
|
+
constructor(detail: PointsLinesMaterialUnsupportedDetail) {
|
|
1188
|
+
super(`Points/Lines material is unsupported on entity ${detail.entity}`);
|
|
1189
|
+
this.name = 'PointsLinesMaterialUnsupportedError';
|
|
1190
|
+
this.hint = `use Materials.unlit without a shadow-caster pass for entity ${detail.entity}`;
|
|
1191
|
+
this.detail = detail;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
export interface PointsLinesBudgetExceededDetail {
|
|
1196
|
+
readonly lane: string;
|
|
1197
|
+
readonly requested: number;
|
|
1198
|
+
readonly limit: number;
|
|
1199
|
+
readonly unit: 'points' | 'segments' | 'vertices' | 'indices' | 'bytes' | 'draws';
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export class PointsLinesBudgetExceededError extends Error {
|
|
1203
|
+
readonly code = 'points-lines-budget-exceeded' as const;
|
|
1204
|
+
readonly expected: string;
|
|
1205
|
+
readonly hint: string;
|
|
1206
|
+
readonly detail: PointsLinesBudgetExceededDetail;
|
|
1207
|
+
|
|
1208
|
+
constructor(detail: PointsLinesBudgetExceededDetail) {
|
|
1209
|
+
super(`Points/Lines ${detail.unit} budget exceeded`);
|
|
1210
|
+
this.name = 'PointsLinesBudgetExceededError';
|
|
1211
|
+
this.expected = `${detail.unit} <= ${detail.limit}`;
|
|
1212
|
+
this.hint = `reduce Points/Lines ${detail.unit} or split the entity into bounded equivalent batches`;
|
|
1213
|
+
this.detail = detail;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export interface PointsLinesPrepareFailedDetail {
|
|
1218
|
+
readonly owner: string;
|
|
1219
|
+
readonly generation: number;
|
|
1220
|
+
readonly stage: 'prepare';
|
|
1221
|
+
readonly cause: string;
|
|
1222
|
+
readonly lastKnownGood: boolean;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
export class PointsLinesPrepareFailedError extends Error {
|
|
1226
|
+
readonly code = 'points-lines-prepare-failed' as const;
|
|
1227
|
+
readonly expected = 'the complete Points/Lines candidate validates before publication';
|
|
1228
|
+
readonly hint: string;
|
|
1229
|
+
readonly detail: PointsLinesPrepareFailedDetail;
|
|
1230
|
+
|
|
1231
|
+
constructor(detail: PointsLinesPrepareFailedDetail) {
|
|
1232
|
+
super(`Points/Lines prepare failed at generation ${detail.generation}`);
|
|
1233
|
+
this.name = 'PointsLinesPrepareFailedError';
|
|
1234
|
+
this.hint = detail.lastKnownGood
|
|
1235
|
+
? 'retain the last known good candidate, repair the producer, and retry prepare'
|
|
1236
|
+
: 'repair the producer and retry prepare; the failed first candidate produces zero draw';
|
|
1237
|
+
this.detail = detail;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1104
1241
|
// -- RenderErrorCode / RenderError closed unions --------------------------------
|
|
1105
1242
|
|
|
1106
1243
|
/**
|
|
@@ -1138,4 +1275,10 @@ export type RenderError =
|
|
|
1138
1275
|
| RenderFeaturePassOrderConflictError
|
|
1139
1276
|
| RenderFeaturePreparationFailedError
|
|
1140
1277
|
| RenderFeaturePreparedStateMismatchError
|
|
1141
|
-
| RenderFeatureDrawRecordingFailedError
|
|
1278
|
+
| RenderFeatureDrawRecordingFailedError
|
|
1279
|
+
| PointsLinesInvalidStyleError
|
|
1280
|
+
| PointsLinesTopologyMismatchError
|
|
1281
|
+
| PointsLinesStyleUnsupportedError
|
|
1282
|
+
| PointsLinesMaterialUnsupportedError
|
|
1283
|
+
| PointsLinesBudgetExceededError
|
|
1284
|
+
| PointsLinesPrepareFailedError;
|