@hraness/slopcamera 3.3.2 → 3.3.3

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/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- import{SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS,SLOPCAMERA_PORTABLE_CAPABILITY_VERSION,SLOPCAMERA_VERSION,STUDIO_LIMITS,SlopcameraCapabilityCommandSchema,SlopcameraCapabilityEffectSchema,SlopcameraCapabilityManifestSchema,SlopcameraCapabilityModuleDeclarationSchema,SlopcameraCapabilityModuleSchema,SlopcameraCapabilityOperationSchema,SlopcameraCapabilityPreparationSchema,SlopcameraCapabilityProfileSchema,SlopcameraCapabilityQualificationSchema,SlopcameraCapabilityResourceSchema,SlopcameraCapabilityResumeSchema,SlopcameraCapabilityToolSchema,SlopcameraCapabilityWorkflowSchema,SlopcameraMcpToolRuntime,SlopcameraSceneVariantsSchema,StudioCapabilityNameSchema,StudioDigestSchema,StudioEngineOptionsSchema,StudioEngineSchema,StudioExecutionProfileSchema,StudioJobSchema,StudioOutputArtifactSchema,StudioOutputFormatSchema,StudioOutputInterpretationSchema,StudioOutputRoleSchema,StudioOutputSpecSchema,StudioPathSchema,StudioPlanSchema,StudioReceiptSchema,StudioRenderSchema,StudioRuntimeIdentitySchema,StudioSourceBundleSchema,StudioSourceSpaceSchema,WorkspaceBoundary,WorkspaceBoundaryError,artifactSummary,checkDiagramFile,compileSlopcameraCapabilityManifest,createPortableSlopcameraCapabilityManifest,defineSlopcameraCapabilityModule,diagramApi,inspectStudioBundle,inspectStudioPlan,mcpMaximumRenderedPixels,mcpMaximumScale,mcpSourceByteLimit,parseSlopcameraCapabilityManifest,parseSlopcameraSceneVariants,parseStudioJob,parseStudioPlan,parseStudioReceipt,parseStudioRuntimeIdentity,parseStudioSourceBundle,planSlopcameraSceneGallery,planStudioJob,readDiagramFile,renderDiagramFile,runMcpServer,slopcameraApi,slopcameraMcpProtocolVersion,slopcameraMcpServerName,slopcameraMcpTools,slopcameraPortableCapabilityManifest,slopcameraSceneGalleryLimits,studioJobSha256,studioOutputPath,studioRuntimeSha256,studioSourceBundleSha256,summarizeSceneVariantPatch,validateStudioReceipt}from"./index-gs48afpt.js";import{bundledSkillPath,installSkill}from"./index-grk47tgs.js";import"./index-zhg36tf9.js";import"./index-3cxf2c58.js";import{SlopcameraWorkflowError,defineSlopcameraWorkflow,runSlopcameraWorkflow}from"./index-296qhpf2.js";import{DiagramValidationError,SlopcameraOperationError,StackLayoutError,builtInIcons,composeSlopcameraImageGallery,critiqueIconRaster,executeSlopcameraOperation,executeSlopcameraOperationWithLease,extractIconLineArt,galleryCandidateId,galleryPromptFor,generateSlopcameraIcon,generateSlopcameraImageGallery,iconPromptFor,iconVisualGateProblems,isSlopcameraOperationCode,lintDiagram,parseDiagramSource,parseDiagramSpec,parseSlopcameraGalleryVary,parseSlopcameraOperationInput,planSlopcameraGallery,renderPng,renderSvg,resolveDiagramSource,resolveEdge,resolveStackLayout,searchSlopcameraOperations,serializeTldr,slopcameraGalleryAxes,slopcameraGalleryKinds,slopcameraGalleryLimits,slopcameraIconCritiqueDefaultModel,slopcameraIconCritiqueTimeoutMs,slopcameraIconDefaultInk,slopcameraIconDefaultRounds,slopcameraIconMaximumRounds,slopcameraIconPanel,slopcameraIconSubjectMaximumBytes,slopcameraOperationCodes,slopcameraOperationHostResourceClaims,slopcameraOperationRegistry,stackLayoutDefaults,withSlopcameraOperationHostAdmission}from"./index-0bbesn4m.js";import{VTRACER_VERSION,VectorizeError,vectorizeDefaultLimits,vectorizeHardLimits,vectorizeImage,vectorizeProfileNames,vtracerReleases}from"./index-qry58nj2.js";import{SlopcameraCloudError,createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraGatewayCredentialStatus,slopcameraImageModels,slopcameraMaximumPromptBytes,slopcameraMaximumRawImageBytes,slopcameraResponseMediaTypes}from"./index-77fjfg9f.js";import{HOST_RESOURCE_MAX_WAIT_MILLISECONDS,HostResourceError,createDefaultHostResourceCoordinator,createHostResourceCoordinator,createProcessLocalHostResourceCoordinator,defaultSlopcameraHostResourceProfile,defaultSlopcameraHostResourceStateRoot,normalizeHostResourceClaims,normalizeHostResourceProfile,slopcameraHostResourceNames}from"./index-b3geqczn.js";import"./index-80a36bc0.js";export{withSlopcameraOperationHostAdmission,vtracerReleases,vectorizeProfileNames,vectorizeImage,vectorizeHardLimits,vectorizeDefaultLimits,validateStudioReceipt,summarizeSceneVariantPatch,studioSourceBundleSha256,studioRuntimeSha256,studioOutputPath,studioJobSha256,stackLayoutDefaults,slopcameraSceneGalleryLimits,slopcameraResponseMediaTypes,slopcameraPortableCapabilityManifest,slopcameraOperationRegistry,slopcameraOperationHostResourceClaims,slopcameraOperationCodes,slopcameraMcpTools,slopcameraMcpServerName,slopcameraMcpProtocolVersion,slopcameraMaximumRawImageBytes,slopcameraMaximumPromptBytes,slopcameraImageModels,slopcameraIconSubjectMaximumBytes,slopcameraIconPanel,slopcameraIconMaximumRounds,slopcameraIconDefaultRounds,slopcameraIconDefaultInk,slopcameraIconCritiqueTimeoutMs,slopcameraIconCritiqueDefaultModel,slopcameraHostResourceNames,slopcameraGatewayCredentialStatus,slopcameraGatewayApiBaseUrl,slopcameraGalleryLimits,slopcameraGalleryKinds,slopcameraGalleryAxes,slopcameraApi,serializeTldr,searchSlopcameraOperations,runSlopcameraWorkflow,runMcpServer,resolveStackLayout,resolveSlopcameraGatewayCredential,resolveEdge,resolveDiagramSource,renderSvg,renderPng,renderDiagramFile,readDiagramFile,planStudioJob,planSlopcameraSceneGallery,planSlopcameraGallery,parseStudioSourceBundle,parseStudioRuntimeIdentity,parseStudioReceipt,parseStudioPlan,parseStudioJob,parseSlopcameraSceneVariants,parseSlopcameraOperationInput,parseSlopcameraGalleryVary,parseSlopcameraCapabilityManifest,parseDiagramSpec,parseDiagramSource,normalizeHostResourceProfile,normalizeHostResourceClaims,mcpSourceByteLimit,mcpMaximumScale,mcpMaximumRenderedPixels,lintDiagram,isSlopcameraOperationCode,installSkill,inspectStudioPlan,inspectStudioBundle,iconVisualGateProblems,iconPromptFor,generateSlopcameraImageGallery,generateSlopcameraImageFile,generateSlopcameraImage,generateSlopcameraIcon,galleryPromptFor,galleryCandidateId,extractIconLineArt,executeSlopcameraOperationWithLease,executeSlopcameraOperation,diagramApi,defineSlopcameraWorkflow,defineSlopcameraCapabilityModule,defaultSlopcameraHostResourceStateRoot,defaultSlopcameraHostResourceProfile,critiqueIconRaster,createProcessLocalHostResourceCoordinator,createPortableSlopcameraCapabilityManifest,createHostResourceCoordinator,createFixedGatewayFetch,createDefaultHostResourceCoordinator,composeSlopcameraImageGallery,compileSlopcameraCapabilityManifest,checkDiagramFile,bundledSkillPath,builtInIcons,artifactSummary,WorkspaceBoundaryError,WorkspaceBoundary,VectorizeError,VTRACER_VERSION,StudioSourceSpaceSchema,StudioSourceBundleSchema,StudioRuntimeIdentitySchema,StudioRenderSchema,StudioReceiptSchema,StudioPlanSchema,StudioPathSchema,StudioOutputSpecSchema,StudioOutputRoleSchema,StudioOutputInterpretationSchema,StudioOutputFormatSchema,StudioOutputArtifactSchema,StudioJobSchema,StudioExecutionProfileSchema,StudioEngineSchema,StudioEngineOptionsSchema,StudioDigestSchema,StudioCapabilityNameSchema,StackLayoutError,SlopcameraWorkflowError,SlopcameraSceneVariantsSchema,SlopcameraOperationError,SlopcameraMcpToolRuntime,SlopcameraCloudError,SlopcameraCapabilityWorkflowSchema,SlopcameraCapabilityToolSchema,SlopcameraCapabilityResumeSchema,SlopcameraCapabilityResourceSchema,SlopcameraCapabilityQualificationSchema,SlopcameraCapabilityProfileSchema,SlopcameraCapabilityPreparationSchema,SlopcameraCapabilityOperationSchema,SlopcameraCapabilityModuleSchema,SlopcameraCapabilityModuleDeclarationSchema,SlopcameraCapabilityManifestSchema,SlopcameraCapabilityEffectSchema,SlopcameraCapabilityCommandSchema,STUDIO_LIMITS,SLOPCAMERA_VERSION,SLOPCAMERA_PORTABLE_CAPABILITY_VERSION,SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS,HostResourceError,HOST_RESOURCE_MAX_WAIT_MILLISECONDS,DiagramValidationError};
2
+ import{SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS,SLOPCAMERA_PORTABLE_CAPABILITY_VERSION,SLOPCAMERA_VERSION,STUDIO_LIMITS,SlopcameraCapabilityCommandSchema,SlopcameraCapabilityEffectSchema,SlopcameraCapabilityManifestSchema,SlopcameraCapabilityModuleDeclarationSchema,SlopcameraCapabilityModuleSchema,SlopcameraCapabilityOperationSchema,SlopcameraCapabilityPreparationSchema,SlopcameraCapabilityProfileSchema,SlopcameraCapabilityQualificationSchema,SlopcameraCapabilityResourceSchema,SlopcameraCapabilityResumeSchema,SlopcameraCapabilityToolSchema,SlopcameraCapabilityWorkflowSchema,SlopcameraMcpToolRuntime,SlopcameraSceneVariantsSchema,StudioCapabilityNameSchema,StudioDigestSchema,StudioEngineOptionsSchema,StudioEngineSchema,StudioExecutionProfileSchema,StudioJobSchema,StudioOutputArtifactSchema,StudioOutputFormatSchema,StudioOutputInterpretationSchema,StudioOutputRoleSchema,StudioOutputSpecSchema,StudioPathSchema,StudioPlanSchema,StudioReceiptSchema,StudioRenderSchema,StudioRuntimeIdentitySchema,StudioSourceBundleSchema,StudioSourceSpaceSchema,WorkspaceBoundary,WorkspaceBoundaryError,artifactSummary,checkDiagramFile,compileSlopcameraCapabilityManifest,createPortableSlopcameraCapabilityManifest,defineSlopcameraCapabilityModule,diagramApi,inspectStudioBundle,inspectStudioPlan,mcpMaximumRenderedPixels,mcpMaximumScale,mcpSourceByteLimit,parseSlopcameraCapabilityManifest,parseSlopcameraSceneVariants,parseStudioJob,parseStudioPlan,parseStudioReceipt,parseStudioRuntimeIdentity,parseStudioSourceBundle,planSlopcameraSceneGallery,planStudioJob,readDiagramFile,renderDiagramFile,runMcpServer,slopcameraApi,slopcameraMcpProtocolVersion,slopcameraMcpServerName,slopcameraMcpTools,slopcameraPortableCapabilityManifest,slopcameraSceneGalleryLimits,studioJobSha256,studioOutputPath,studioRuntimeSha256,studioSourceBundleSha256,summarizeSceneVariantPatch,validateStudioReceipt}from"./index-3e1pcxb5.js";import{bundledSkillPath,installSkill}from"./index-grk47tgs.js";import"./index-z1kd1x8j.js";import"./index-3cxf2c58.js";import{SlopcameraWorkflowError,defineSlopcameraWorkflow,runSlopcameraWorkflow}from"./index-296qhpf2.js";import{DiagramValidationError,SlopcameraOperationError,StackLayoutError,builtInIcons,composeSlopcameraImageGallery,critiqueIconRaster,executeSlopcameraOperation,executeSlopcameraOperationWithLease,extractIconLineArt,galleryCandidateId,galleryPromptFor,generateSlopcameraIcon,generateSlopcameraImageGallery,iconPromptFor,iconVisualGateProblems,isSlopcameraOperationCode,lintDiagram,parseDiagramSource,parseDiagramSpec,parseSlopcameraGalleryVary,parseSlopcameraOperationInput,planSlopcameraGallery,renderPng,renderSvg,resolveDiagramSource,resolveEdge,resolveStackLayout,searchSlopcameraOperations,serializeTldr,slopcameraGalleryAxes,slopcameraGalleryKinds,slopcameraGalleryLimits,slopcameraIconCritiqueDefaultModel,slopcameraIconCritiqueTimeoutMs,slopcameraIconDefaultInk,slopcameraIconDefaultRounds,slopcameraIconMaximumRounds,slopcameraIconPanel,slopcameraIconSubjectMaximumBytes,slopcameraOperationCodes,slopcameraOperationHostResourceClaims,slopcameraOperationRegistry,stackLayoutDefaults,withSlopcameraOperationHostAdmission}from"./index-0bbesn4m.js";import{VTRACER_VERSION,VectorizeError,vectorizeDefaultLimits,vectorizeHardLimits,vectorizeImage,vectorizeProfileNames,vtracerReleases}from"./index-qry58nj2.js";import{SlopcameraCloudError,createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraGatewayCredentialStatus,slopcameraImageModels,slopcameraMaximumPromptBytes,slopcameraMaximumRawImageBytes,slopcameraResponseMediaTypes}from"./index-77fjfg9f.js";import{HOST_RESOURCE_MAX_WAIT_MILLISECONDS,HostResourceError,createDefaultHostResourceCoordinator,createHostResourceCoordinator,createProcessLocalHostResourceCoordinator,defaultSlopcameraHostResourceProfile,defaultSlopcameraHostResourceStateRoot,normalizeHostResourceClaims,normalizeHostResourceProfile,slopcameraHostResourceNames}from"./index-b3geqczn.js";import"./index-80a36bc0.js";export{withSlopcameraOperationHostAdmission,vtracerReleases,vectorizeProfileNames,vectorizeImage,vectorizeHardLimits,vectorizeDefaultLimits,validateStudioReceipt,summarizeSceneVariantPatch,studioSourceBundleSha256,studioRuntimeSha256,studioOutputPath,studioJobSha256,stackLayoutDefaults,slopcameraSceneGalleryLimits,slopcameraResponseMediaTypes,slopcameraPortableCapabilityManifest,slopcameraOperationRegistry,slopcameraOperationHostResourceClaims,slopcameraOperationCodes,slopcameraMcpTools,slopcameraMcpServerName,slopcameraMcpProtocolVersion,slopcameraMaximumRawImageBytes,slopcameraMaximumPromptBytes,slopcameraImageModels,slopcameraIconSubjectMaximumBytes,slopcameraIconPanel,slopcameraIconMaximumRounds,slopcameraIconDefaultRounds,slopcameraIconDefaultInk,slopcameraIconCritiqueTimeoutMs,slopcameraIconCritiqueDefaultModel,slopcameraHostResourceNames,slopcameraGatewayCredentialStatus,slopcameraGatewayApiBaseUrl,slopcameraGalleryLimits,slopcameraGalleryKinds,slopcameraGalleryAxes,slopcameraApi,serializeTldr,searchSlopcameraOperations,runSlopcameraWorkflow,runMcpServer,resolveStackLayout,resolveSlopcameraGatewayCredential,resolveEdge,resolveDiagramSource,renderSvg,renderPng,renderDiagramFile,readDiagramFile,planStudioJob,planSlopcameraSceneGallery,planSlopcameraGallery,parseStudioSourceBundle,parseStudioRuntimeIdentity,parseStudioReceipt,parseStudioPlan,parseStudioJob,parseSlopcameraSceneVariants,parseSlopcameraOperationInput,parseSlopcameraGalleryVary,parseSlopcameraCapabilityManifest,parseDiagramSpec,parseDiagramSource,normalizeHostResourceProfile,normalizeHostResourceClaims,mcpSourceByteLimit,mcpMaximumScale,mcpMaximumRenderedPixels,lintDiagram,isSlopcameraOperationCode,installSkill,inspectStudioPlan,inspectStudioBundle,iconVisualGateProblems,iconPromptFor,generateSlopcameraImageGallery,generateSlopcameraImageFile,generateSlopcameraImage,generateSlopcameraIcon,galleryPromptFor,galleryCandidateId,extractIconLineArt,executeSlopcameraOperationWithLease,executeSlopcameraOperation,diagramApi,defineSlopcameraWorkflow,defineSlopcameraCapabilityModule,defaultSlopcameraHostResourceStateRoot,defaultSlopcameraHostResourceProfile,critiqueIconRaster,createProcessLocalHostResourceCoordinator,createPortableSlopcameraCapabilityManifest,createHostResourceCoordinator,createFixedGatewayFetch,createDefaultHostResourceCoordinator,composeSlopcameraImageGallery,compileSlopcameraCapabilityManifest,checkDiagramFile,bundledSkillPath,builtInIcons,artifactSummary,WorkspaceBoundaryError,WorkspaceBoundary,VectorizeError,VTRACER_VERSION,StudioSourceSpaceSchema,StudioSourceBundleSchema,StudioRuntimeIdentitySchema,StudioRenderSchema,StudioReceiptSchema,StudioPlanSchema,StudioPathSchema,StudioOutputSpecSchema,StudioOutputRoleSchema,StudioOutputInterpretationSchema,StudioOutputFormatSchema,StudioOutputArtifactSchema,StudioJobSchema,StudioExecutionProfileSchema,StudioEngineSchema,StudioEngineOptionsSchema,StudioDigestSchema,StudioCapabilityNameSchema,StackLayoutError,SlopcameraWorkflowError,SlopcameraSceneVariantsSchema,SlopcameraOperationError,SlopcameraMcpToolRuntime,SlopcameraCloudError,SlopcameraCapabilityWorkflowSchema,SlopcameraCapabilityToolSchema,SlopcameraCapabilityResumeSchema,SlopcameraCapabilityResourceSchema,SlopcameraCapabilityQualificationSchema,SlopcameraCapabilityProfileSchema,SlopcameraCapabilityPreparationSchema,SlopcameraCapabilityOperationSchema,SlopcameraCapabilityModuleSchema,SlopcameraCapabilityModuleDeclarationSchema,SlopcameraCapabilityManifestSchema,SlopcameraCapabilityEffectSchema,SlopcameraCapabilityCommandSchema,STUDIO_LIMITS,SLOPCAMERA_VERSION,SLOPCAMERA_PORTABLE_CAPABILITY_VERSION,SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS,HostResourceError,HOST_RESOURCE_MAX_WAIT_MILLISECONDS,DiagramValidationError};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/slopcamera",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
4
4
  "description": "A local visual studio for coding agents: author scenes, combine generated and recorded media, and export images, diagrams, animation, and video from retained sources.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,7 +14,7 @@ command -v slopcamera
14
14
 
15
15
  A restricted shell can omit package-manager paths. Check known host installation paths before declaring a tool unavailable. If Bun is genuinely absent, follow its official [installation guide](https://bun.sh/docs/installation) within the user’s authorized setup scope. Do not switch package managers or pipe an unreviewed installer into a shell.
16
16
 
17
- Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.1/hraness-slopcamera-3.3.1.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
17
+ Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.2/hraness-slopcamera-3.3.2.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
18
18
 
19
19
  Use an existing compatible source build when available. Otherwise follow the [complete source-install guide](https://github.com/hraness/slopcamera/blob/main/docs/how-to/use-current-source.md): clone into a new directory, record its exact commit, install locked dependencies without lifecycle scripts, build the SDK and source CLI, then define the shell command against that checkout. A clone or skill installation alone does not install the executable. Do not alter an existing active checkout to satisfy this path.
20
20
 
@@ -1,6 +1,6 @@
1
1
  # Build a revisable parametric design
2
2
 
3
- Use Slopcamera v3.3.1 or newer and `slopcamera scene design` for dimensions, repeated architectural parts, furniture, pavilions and façades that must regenerate from retained rules. This is a local mesh-design workflow; Rhino, Grasshopper and native engine execution are separate tools.
3
+ Use Slopcamera v3.3.1 or newer and `slopcamera scene design` for dimensions, repeated architectural parts, furniture, pavilions and façades that must regenerate from retained rules. This workflow uses Slopcamera's local mesh geometry and needs no additional modeling application.
4
4
 
5
5
  1. Discover original studies with `slopcamera scene design catalog --json`. Start with `scene design init <new-directory> --template crescent-pavilion|spiral-stair|ribbed-tower|modular-bookshelf`.
6
6
  2. Read the emitted `design.json` and inspect it with `scene design inspect`. Describe the user's intended shape in named parameters, derived scalar values, constraints and semantic geometry stages. Keep references to engineering knowledge separate from visual references. Do not infer structural validation from appearance.
@@ -12,3 +12,7 @@ Use Slopcamera v3.3.1 or newer and `slopcamera scene design` for dimensions, rep
12
12
  Place generated bundles below `artifacts/slopcamera/generated/`. Existing files conflict. An interrupted bundle without its final receipt is incomplete; inspect it and choose a fresh destination. The design compiler never executes source text. For native CAD operations or Blender/Cycles production shading, retain a native-studio source and use the explicit trusted-code workflow in [native studio](native-studio.md).
13
13
 
14
14
  Detailed contracts and checked commands: [design reference](https://github.com/hraness/slopcamera/blob/main/docs/parametric-design.md) and [design guide](https://github.com/hraness/slopcamera/blob/main/docs/how-to/parametric-design.md). The portable SDK exports the design compiler and starter catalog from `@hraness/slopcamera/code`; file publication and rendering belong to the CLI.
15
+
16
+ For an existing film, compile and decode assets before playback, retain exact bytes and receipts, and preserve the host's camera and interaction timing. A custom renderer must apply primitive and entity transforms once and preserve the SDK's linear material colors. Cache geometry but clone it before changing UVs; use world-scale texture coordinates for consistent finishes. Review actual camera paths for obstructing members, supported roof details, aperture depth, prop contacts and coplanar floor flicker. Verify repeated seeks and inspect the final encoded movie, not only hero stills. See the [film integration guide](https://github.com/hraness/slopcamera/blob/main/docs/how-to/parametric-design.md#integrate-a-design-into-an-existing-film).
17
+
18
+ For wall openings, `center[1]` is sill height from the wall base; arch height includes its semicircular crown. Use v3.3.3 or newer for corrected openings and boolean surfaces. Boolean inputs must be closed and satisfy hull convexity; conservative geometry budgets can reject opening counts below the schema maximum. Split a complex façade into wall segments. Distant wall LOD and collision proxies remain solid boxes; do not use them as aperture-accurate interaction geometry.
@@ -34,6 +34,8 @@ Prefer helpers over hand-written scene JSON. The `@hraness/slopcamera/code` expo
34
34
 
35
35
  `slopcamera scene asset admit model.glb --source-root <dir> --output manifest.json --json` validates a local GLB 2.0 against the closed profile, stores it content-addressed, derives model-space and scene-space bounds into a sibling facts manifest, and emits the mesh entity plus ready `add-asset`/`add-entity` patch operations. The source path must stay inside `--source-root`; unsupported GLB features reject rather than degrade. Apply the returned operations through `scene patch`, then feed the admission document itself to `scene audit --asset-bounds`.
36
36
 
37
+ Downloaded props use this same workflow. Require a self-contained GLB; any textures must be embedded PNG/JPEG. Loose glTF bundles, external textures, compressed meshes, and unsupported attributes or extensions reject. Admission defaults to a gray entity material. To preserve the model's materials and textures, set `geometry.materialMode: "source"` on the returned mesh in its `add-entity` operation before applying the patch, then inspect a rendered frame. Check each model's license and admission result; see the [local asset guide](https://github.com/hraness/slopcamera/blob/main/docs/spatial-scenes.md#admit-a-local-gltf-asset).
38
+
37
39
  ## Refine an existing scene
38
40
 
39
41
  Use this pass when an existing HTML, Three.js, or native scene needs better
@@ -44,6 +44,8 @@ export const SPATIAL_GEOMETRY_LIMITS = Object.freeze({
44
44
 
45
45
  export const SPATIAL_GEOMETRY_GRAPH_KIND = "slopcamera.spatial-geometry-graph" as const
46
46
  export const SPATIAL_GEOMETRY_PROFILE = "slopcamera.parametric-geometry-v1" as const
47
+ /** CSG algorithm identity is separate from the retained GLB/facts format. */
48
+ export const SPATIAL_GEOMETRY_BOOLEAN_COMPILER = "slopcamera.geometry-boolean-v2" as const
47
49
 
48
50
  const finite = z.number().finite().min(-SPATIAL_GEOMETRY_LIMITS.coordinate).max(SPATIAL_GEOMETRY_LIMITS.coordinate)
49
51
  const positive = z.number().finite().min(1e-9).max(SPATIAL_GEOMETRY_LIMITS.coordinate)
@@ -120,6 +122,7 @@ export interface SpatialGeometryEvaluation {
120
122
  readonly mesh: SpatialGeometryMesh
121
123
  readonly estimate: SpatialGeometryEstimate
122
124
  readonly graphSha256: string
125
+ readonly booleanCompiler?: typeof SPATIAL_GEOMETRY_BOOLEAN_COMPILER
123
126
  }
124
127
 
125
128
  function fail(message: string, path = "geometry"): never {
@@ -747,6 +750,13 @@ function partitionTriangles(input: Builder, planes: readonly Plane[]): Fragment[
747
750
  for (const plane of planes) {
748
751
  const next: Fragment[] = []
749
752
  for (const fragment of fragments) {
753
+ const signed = fragment.positions.map(point => plane.normal[0] * point[0] + plane.normal[1] * point[1] + plane.normal[2] * point[2] - plane.d)
754
+ // A one-sided or coplanar polygon is already a single cell. Clipping
755
+ // it into both inclusive half-spaces duplicates coplanar surfaces.
756
+ if (!signed.some(value => value < -PLANE_EPS) || !signed.some(value => value > PLANE_EPS)) {
757
+ next.push(fragment)
758
+ continue
759
+ }
750
760
  const inner = clipFragment(fragment, plane, true)
751
761
  const outer = clipFragment(fragment, plane, false)
752
762
  if (inner !== null) next.push(inner)
@@ -759,25 +769,59 @@ function partitionTriangles(input: Builder, planes: readonly Plane[]): Fragment[
759
769
  return result
760
770
  }
761
771
 
762
- /** Hull-solid membership: the fragment centroid must satisfy every support plane. */
763
- function fragmentInside(planes: readonly Plane[], fragment: Fragment): boolean {
772
+ function fragmentCentroid(fragment: Fragment): Vec3 {
764
773
  const count = fragment.positions.length
765
- const centroid: Vec3 = [
774
+ return [
766
775
  fragment.positions.reduce((sum, p) => sum + p[0], 0) / count,
767
776
  fragment.positions.reduce((sum, p) => sum + p[1], 0) / count,
768
777
  fragment.positions.reduce((sum, p) => sum + p[2], 0) / count,
769
778
  ]
770
- return planes.every(plane => plane.normal[0] * centroid[0] + plane.normal[1] * centroid[1] + plane.normal[2] * centroid[2] - plane.d <= PLANE_EPS)
779
+ }
780
+
781
+ function fragmentNormal(fragment: Fragment): Vec3 | null {
782
+ const origin = fragment.positions[0]!, normal = [0, 0, 0]
783
+ for (let index = 1; index + 1 < fragment.positions.length; index++) {
784
+ const a = fragment.positions[index]!, b = fragment.positions[index + 1]!
785
+ const u = [a[0] - origin[0], a[1] - origin[1], a[2] - origin[2]], v = [b[0] - origin[0], b[1] - origin[1], b[2] - origin[2]]
786
+ normal[0] = normal[0]! + u[1]! * v[2]! - u[2]! * v[1]!
787
+ normal[1] = normal[1]! + u[2]! * v[0]! - u[0]! * v[2]!
788
+ normal[2] = normal[2]! + u[0]! * v[1]! - u[1]! * v[0]!
789
+ }
790
+ const length = Math.hypot(...normal)
791
+ return length < 1e-12 ? null : [normal[0]! / length, normal[1]! / length, normal[2]! / length]
792
+ }
793
+
794
+ function planeDistance(plane: Plane, point: Vec3): number {
795
+ return plane.normal[0] * point[0] + plane.normal[1] * point[1] + plane.normal[2] * point[2] - plane.d
796
+ }
797
+
798
+ /** Symbolic infinitesimal occupancy on each side avoids scale-dependent probe distances. */
799
+ function insideHullSide(planes: readonly Plane[], point: Vec3, normal: Vec3, side: number): boolean {
800
+ return planes.every(plane => {
801
+ const signed = planeDistance(plane, point)
802
+ if (signed > PLANE_EPS) return false
803
+ if (signed < -PLANE_EPS) return true
804
+ return side * (plane.normal[0] * normal[0] + plane.normal[1] * normal[1] + plane.normal[2] * normal[2]) <= PLANE_EPS
805
+ })
806
+ }
807
+
808
+ function boundsTouch(a: Bounds, b: Bounds): boolean {
809
+ return [0, 1, 2].every(axis => a.min[axis]! <= b.max[axis]! + PLANE_EPS && b.min[axis]! <= a.max[axis]! + PLANE_EPS)
810
+ }
811
+
812
+ function builderBounds(input: Builder): Bounds {
813
+ const low: number[] = [Infinity, Infinity, Infinity], high: number[] = [-Infinity, -Infinity, -Infinity]
814
+ for (let i = 0; i < input.positions.length; i++) {
815
+ const axis = i % 3, value = input.positions[i]!
816
+ low[axis] = Math.min(low[axis]!, value); high[axis] = Math.max(high[axis]!, value)
817
+ }
818
+ return { min: low as unknown as Vec3, max: high as unknown as Vec3 }
771
819
  }
772
820
 
773
821
  function emitFragments(b: Builder, fragments: readonly Fragment[], flip: boolean): void {
774
822
  for (const fragment of fragments) {
775
- let z = 0
776
- for (let index = 0; index < fragment.positions.length; index++) {
777
- const a = fragment.positions[index]!, c = fragment.positions[(index + 1) % fragment.positions.length]!
778
- z += a[0] * c[1] - a[1] * c[0]
779
- }
780
- const unit: Vec3 = z < 0 ? [0, 0, -1] : [0, 0, 1]
823
+ const unit = fragmentNormal(fragment)
824
+ if (unit === null) continue
781
825
  const oriented: Vec3 = flip ? [-unit[0], -unit[1], -unit[2]] : unit
782
826
  const indices = fragment.positions.map((position, index) => vertex(b, position, oriented, fragment.uvs[index]!))
783
827
  for (let index = 1; index + 1 < indices.length; index++) {
@@ -801,30 +845,31 @@ function booleanMesh(input: Extract<SpatialGeometryNode, { kind: "boolean" }>, m
801
845
  fail("Boolean inputs exceed the bounded simple-input triangle budget.", `${path}.${index}`)
802
846
  }
803
847
  }
804
- const planesA = uniquePlanes(a, path)
805
- const planesC = cutters.map(cutter => uniquePlanes(cutter, path))
806
- assertHull(a, planesA, path)
807
- cutters.forEach((cutter, index) => assertHull(cutter, planesC[index]!, path))
848
+ const inputs = [a, ...cutters], hulls = inputs.map(mesh => uniquePlanes(mesh, path)), bounds = inputs.map(builderBounds)
849
+ inputs.forEach((mesh, index) => assertHull(mesh, hulls[index]!, path))
808
850
  const output = builder()
809
- if (input.operation === "difference") {
810
- const allCutterPlanes = planesC.flat()
811
- const kept = partitionTriangles(a, allCutterPlanes).filter(piece => !planesC.some(planes => fragmentInside(planes, piece)))
812
- emitFragments(output, kept, false)
813
- for (const [index, cutter] of cutters.entries()) {
814
- const caps = partitionTriangles(cutter, planesA).filter(piece =>
815
- fragmentInside(planesA, piece) && !planesC.some((other, otherIndex) => otherIndex !== index && fragmentInside(other, piece)))
816
- emitFragments(output, caps, true)
817
- }
818
- } else {
819
- const planesB = planesC[0]!, b = cutters[0]!
820
- const piecesA = partitionTriangles(a, planesB), piecesB = partitionTriangles(b, planesA)
821
- if (input.operation === "intersection") {
822
- emitFragments(output, piecesA.filter(piece => fragmentInside(planesB, piece)), false)
823
- emitFragments(output, piecesB.filter(piece => fragmentInside(planesA, piece)), false)
824
- } else {
825
- emitFragments(output, piecesA.filter(piece => !fragmentInside(planesB, piece)), false)
826
- emitFragments(output, piecesB.filter(piece => !fragmentInside(planesA, piece)), false)
851
+ for (const [owner, mesh] of inputs.entries()) {
852
+ const planes = hulls.flatMap((hull, index) => index !== owner && boundsTouch(bounds[owner]!, bounds[index]!) ? hull : [])
853
+ const outward: Fragment[] = [], reversed: Fragment[] = []
854
+ for (const fragment of partitionTriangles(mesh, planes)) {
855
+ const normal = fragmentNormal(fragment)
856
+ if (normal === null) continue
857
+ const point = fragmentCentroid(fragment)
858
+ const occupied = (side: number): boolean => {
859
+ const inside = hulls.map(hull => insideHullSide(hull, point, normal, side))
860
+ return input.operation === "difference" ? inside[0]! && !inside.slice(1).some(Boolean)
861
+ : input.operation === "intersection" ? inside.every(Boolean) : inside.some(Boolean)
862
+ }
863
+ const positive = occupied(1), negative = occupied(-1)
864
+ if (positive === negative) continue
865
+ // Coincident boundary faces belong to the earliest source hull. Their
866
+ // occupancy is shared; retaining both would double area and cause z-fighting.
867
+ const duplicate = hulls.slice(0, owner).some(hull => hull.every(plane => planeDistance(plane, point) <= PLANE_EPS)
868
+ && hull.some(plane => Math.abs(planeDistance(plane, point)) <= PLANE_EPS
869
+ && Math.abs(plane.normal[0] * normal[0] + plane.normal[1] * normal[1] + plane.normal[2] * normal[2]) > 1 - 1e-7))
870
+ if (!duplicate) (positive ? reversed : outward).push(fragment)
827
871
  }
872
+ emitFragments(output, outward, false); emitFragments(output, reversed, true)
828
873
  }
829
874
  output.uvsSet = [a, ...cutters].some(mesh => mesh.uvsSet)
830
875
  if (output.indices.length === 0) fail("Boolean result is empty; the inputs do not overlap as the operation requires.", path)
@@ -1010,8 +1055,15 @@ function estimateNode(node: SpatialGeometryNode, meshEstimates: ReadonlyMap<stri
1010
1055
  case "boolean": {
1011
1056
  const a = mesh(node.a)
1012
1057
  const cutters = node.operation === "difference" ? node.cutters.map(mesh) : [mesh(node.b)]
1013
- const triangles = a.triangles * (1 + cutters.reduce((sum, cutter) => sum + cutter.planes, 0))
1014
- + cutters.reduce((sum, cutter) => sum + cutter.triangles * (a.planes + 1), 0)
1058
+ const inputs = [a, ...cutters]
1059
+ // In one source triangle, n plane intersections form at most
1060
+ // 1+n(n+1)/2 cells. Each proper split adds at most two fan triangles,
1061
+ // hence 1+n(n+1) triangles per input triangle, including all cap faces.
1062
+ // Disjoint input bounds cannot cut each other's source triangles.
1063
+ const triangles = inputs.reduce((total, source, index) => {
1064
+ const planes = inputs.reduce((sum, other, otherIndex) => sum + (otherIndex !== index && boundsTouch(source.bounds, other.bounds) ? other.planes : 0), 0)
1065
+ return total + source.triangles * (1 + planes * (planes + 1))
1066
+ }, 0)
1015
1067
  const other = cutters[0]!
1016
1068
  const bounds = node.operation === "intersection"
1017
1069
  ? { min: [Math.max(a.bounds.min[0], other.bounds.min[0]), Math.max(a.bounds.min[1], other.bounds.min[1]), Math.max(a.bounds.min[2], other.bounds.min[2])] as unknown as Vec3,
@@ -1099,7 +1151,8 @@ export function evaluateSpatialGeometry(input: unknown): SpatialGeometryEvaluati
1099
1151
  if (mesh.vertices > estimate.vertices || mesh.triangles > estimate.triangles) {
1100
1152
  fail("Evaluated mesh exceeded its static budget estimate.", "geometry.output")
1101
1153
  }
1102
- return deepFreezeJson({ mesh, estimate, graphSha256: spatialValueSha256({ domain: "slopcamera.spatial-geometry-graph.v1", graph }) })
1154
+ return deepFreezeJson({ mesh, estimate, graphSha256: spatialValueSha256({ domain: "slopcamera.spatial-geometry-graph.v1", graph }),
1155
+ ...(graph.nodes.some(node => node.kind === "boolean") ? { booleanCompiler: SPATIAL_GEOMETRY_BOOLEAN_COMPILER } : {}) })
1103
1156
  }
1104
1157
 
1105
1158
  // ---------------------------------------------------------------------------
@@ -18,7 +18,7 @@ import {
18
18
  import { SpatialAssetFactsV1Schema, SpatialCollisionProxySchema, type SpatialAssetFactsV1, type SpatialCollisionProxy, type SpatialRetainedArtifact } from "./asset-admission.js"
19
19
  import { mulberry32 } from "./build.js"
20
20
  import {
21
- SPATIAL_GEOMETRY_PROFILE, SpatialGeometryGraphSchema, emitSpatialGeometryGlb, evaluateSpatialGeometry, estimateSpatialGeometryGraph, parseSpatialGeometryGraph,
21
+ SPATIAL_GEOMETRY_PROFILE, SPATIAL_GEOMETRY_BOOLEAN_COMPILER, SpatialGeometryGraphSchema, emitSpatialGeometryGlb, evaluateSpatialGeometry, estimateSpatialGeometryGraph, parseSpatialGeometryGraph,
22
22
  type SpatialGeometryMesh,
23
23
  } from "./geometry.js"
24
24
  import {
@@ -68,6 +68,8 @@ const specVec3 = z.tuple([coordinate, coordinate, coordinate])
68
68
  const editable = z.array(z.enum(["color", "opacity", "transform"])).max(3).optional()
69
69
  const partPlacement = { transform: SpatialTransformSchema.optional(), editable, castShadow: z.boolean().optional(), receiveShadow: z.boolean().optional() }
70
70
 
71
+ /** center is [horizontal center, sill height from the wall base]. Arch height
72
+ * includes the semicircular crown, whose radius is half its width. */
71
73
  const opening = z.discriminatedUnion("kind", [
72
74
  z.strictObject({ kind: z.literal("rect"), center: specVec2, width: meter, height: meter }),
73
75
  z.strictObject({ kind: z.literal("arch"), center: specVec2, width: meter, height: meter }),
@@ -155,6 +157,9 @@ function boxBounds(bounds: Bounds): SpatialCollisionProxy {
155
157
  }
156
158
 
157
159
  const LOD1_DISTANCE_M = 25
160
+ // Wall lowering changed while the retained GLB/facts format remains v1. Keep
161
+ // the revision local to this planner so unrelated generators retain identity.
162
+ export const SPATIAL_PARAMETRIC_WALL_COMPILER = "slopcamera.parametric-wall-v2" as const
158
163
 
159
164
  function planWall(spec: Extract<SpatialParametricSpec, { kind: "wall" }>): PartPlan[] {
160
165
  const { length, height, thickness } = spec
@@ -165,19 +170,33 @@ function planWall(spec: Extract<SpatialParametricSpec, { kind: "wall" }>): PartP
165
170
  if (cx - item.width / 2 < -length / 2 || cx + item.width / 2 > length / 2 || baseY < 0 || baseY + item.height > height) {
166
171
  pfail(`Wall opening ${index} must stay inside the wall rectangle.`, "spec.openings")
167
172
  }
168
- const rectId = `cut${index}`
169
- cutterNodes.push({ id: rectId, kind: "box", size: [item.width, item.height, thickness * 4] })
170
- cutterNodes.push({ id: `${rectId}p`, kind: "transform", input: rectId, transform: { position: [cx, baseY + item.height / 2, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] } })
171
- cutterIds.push(`${rectId}p`)
173
+ const cutterId = `cut${index}`
174
+ // The body is still centered until the final lift. Authored opening Y is
175
+ // measured from its base, so cutters must share the body's local frame.
176
+ let cutterY = baseY + item.height / 2 - height / 2
172
177
  if (item.kind === "arch") {
173
178
  const radius = item.width / 2
174
179
  if (radius > item.height) pfail(`Arch opening ${index} rise exceeds its height.`, "spec.openings")
175
- const cylId = `cyl${index}`
176
- // Half-cylinder lintel: cylinder rotated so its axis runs through the wall thickness.
177
- cutterNodes.push({ id: cylId, kind: "cylinder", radius, height: thickness * 4, segments: 24 })
178
- cutterNodes.push({ id: `${cylId}r`, kind: "transform", input: cylId, transform: { position: [cx, baseY + item.height - radius, 0], rotation: [Math.SQRT1_2, 0, 0, Math.SQRT1_2], scale: [1, 1, 1] } })
179
- cutterIds.push(`${cylId}r`)
180
+ const springline = item.height - radius
181
+ // One extruded cutter has a flat sill and a semicircular crown. A full
182
+ // circle would cut below a shallow opening, while a full-height box
183
+ // would remove the masonry that should remain at the arch shoulders.
184
+ const points: number[][] = [[-radius, 0], [radius, 0]]
185
+ if (springline > 0) points.push([radius, springline])
186
+ for (let segment = 1; segment <= 12; segment++) {
187
+ const angle = Math.PI * segment / 12
188
+ points.push(segment === 12 ? [-radius, springline] : [radius * Math.cos(angle), springline + radius * Math.sin(angle)])
189
+ }
190
+ // At height == radius the arc ends at the first profile point.
191
+ if (springline === 0) points.pop()
192
+ cutterNodes.push({ id: `${cutterId}profile`, kind: "profile", points })
193
+ cutterNodes.push({ id: cutterId, kind: "extrude", profile: `${cutterId}profile`, depth: thickness * 4 })
194
+ cutterY = baseY - height / 2
195
+ } else {
196
+ cutterNodes.push({ id: cutterId, kind: "box", size: [item.width, item.height, thickness * 4] })
180
197
  }
198
+ cutterNodes.push({ id: `${cutterId}p`, kind: "transform", input: cutterId, transform: { position: [cx, cutterY, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] } })
199
+ cutterIds.push(`${cutterId}p`)
181
200
  }
182
201
  const nodes: unknown[] = [{ id: "body", kind: "box", size: [length, height, thickness] }]
183
202
  let output = "body"
@@ -534,6 +553,8 @@ export interface SpatialParametricArtifact { readonly assetId: string; readonly
534
553
  export interface SpatialParametricFacts { readonly manifest: SpatialAssetManifest; readonly facts: SpatialAssetFactsV1 }
535
554
  export interface SpatialParametricReceipt {
536
555
  readonly name?: string
556
+ readonly compiler?: typeof SPATIAL_PARAMETRIC_WALL_COMPILER
557
+ readonly booleanCompiler?: typeof SPATIAL_GEOMETRY_BOOLEAN_COMPILER
537
558
  readonly kind: "slopcamera.spatial-parametric-receipt"
538
559
  readonly schemaVersion: 1
539
560
  readonly generatorId: string
@@ -580,14 +601,19 @@ export function emitSpatialParametric(input: unknown): SpatialParametricOutput {
580
601
  outputs: [...outputs],
581
602
  })
582
603
  }
583
- const sourceSha256 = spatialValueSha256({ domain: "slopcamera.parametric-source.v1", profile: SPATIAL_GEOMETRY_PROFILE, kind: spec.kind })
584
- const parametersSha256 = spatialGeneratorParametersSha256(spec)
585
- const specSha256 = spatialValueSha256({ domain: "slopcamera.parametric-spec.v1", spec })
586
- const seed = request.seed ?? deriveSpatialGeneratorSeed(sourceSha256)
587
- const runtimeSha256 = spatialValueSha256({ domain: "slopcamera.parametric-runtime.v1", profile: SPATIAL_GEOMETRY_PROFILE })
588
604
  const plans = PLANNERS[spec.kind](spec as never)
589
605
  if (plans.length > SPATIAL_PARAMETRIC_LIMITS.parts) pfail("Spec expands beyond the part budget.", "spec")
590
606
  validatePartMaterials(plans)
607
+ const usesBoolean = plans.some(part => part.lods.some(lod => parseSpatialGeometryGraph(lod.graph).nodes.some(node => node.kind === "boolean")))
608
+ const compiler: Pick<SpatialParametricReceipt, "compiler" | "booleanCompiler"> = {
609
+ ...(spec.kind === "wall" ? { compiler: SPATIAL_PARAMETRIC_WALL_COMPILER } : {}),
610
+ ...(usesBoolean ? { booleanCompiler: SPATIAL_GEOMETRY_BOOLEAN_COMPILER } : {}),
611
+ }
612
+ const sourceSha256 = spatialValueSha256({ domain: "slopcamera.parametric-source.v1", profile: SPATIAL_GEOMETRY_PROFILE, kind: spec.kind, ...compiler })
613
+ const parametersSha256 = spatialGeneratorParametersSha256(spec)
614
+ const specSha256 = spatialValueSha256({ domain: "slopcamera.parametric-spec.v1", spec })
615
+ const seed = request.seed ?? deriveSpatialGeneratorSeed(sourceSha256)
616
+ const runtimeSha256 = spatialValueSha256({ domain: "slopcamera.parametric-runtime.v1", profile: SPATIAL_GEOMETRY_PROFILE, ...compiler })
591
617
  const manifests: SpatialAssetManifest[] = []
592
618
  const artifacts: SpatialParametricArtifact[] = []
593
619
  const entities: SpatialEntity[] = []
@@ -682,6 +708,7 @@ export function emitSpatialParametric(input: unknown): SpatialParametricOutput {
682
708
  generatorId, specSha256, parametersSha256, seed,
683
709
  ...(request.name === undefined ? {} : { name: request.name }),
684
710
  profile: SPATIAL_GEOMETRY_PROFILE,
711
+ ...compiler,
685
712
  assets: receiptAssets,
686
713
  entities: entities.map(entity => entity.entityId).sort(),
687
714
  native: retainedArtifacts.map(artifact => ({ requestSha256: artifact.requestSha256, receiptSha256: artifact.receiptSha256 })),
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SLOPCAMERA_VERSION = "3.3.2" as const
1
+ export const SLOPCAMERA_VERSION = "3.3.3" as const