//	Draw4DRenderer.swift
//
//	© 2025 by Jeff Weeks
//	See TermsOfUse.txt

import Metal

	
//	gHueKata and gHueAna specify the hues
//	for the most kataward and anaward points.
let gHueKata = 0.00
let gHueAna  = 0.75	//	purple (not magenta)

//	gFlashRateFast shouldn't be an obvious rational multiple
//	of gFlashRateSlow, to avoid visible resonances.
let gFlashRateSlow = 8.0					//	radians/sec
let gFlashRateFast = 4.854 * gFlashRateSlow	//	radians/sec


class Draw4DRenderer: GeometryGamesRenderer<Draw4DDocument> {

	//	Using 16-bit integers to index mesh vertices works great
	//	with the current implementation of 4D Draw, and makes
	//	good use of the GPU's power-efficient 16-bit arithmetic.
	//	But if we ever needed to create meshes with 2¹⁶ = 65536
	//	or more vertices (for example for extra-smooth spheres),
	//	we'd need to index the mesh vertices with 32-bit integers instead.
	typealias Draw4DMeshIndexType = UInt16	//	UInt16 or UInt32

	//	Before calling the superclass's (GeometryGamesRenderer's) init()
	//	to set up itsDevice, itsColorPixelFormat, itsCommandQueue, etc.,
	//	and indeed even before calling any of our own methods to set up
	//	the pipeline states, the meshes, etc., we must ensure that
	//	all our instances variables have values.  For that reason,
	//	we declare them to be optionals, which are automatically initialized
	//	to nil.  On the other hand, unwrapping such optionals quickly
	//	becomes tedious, and makes the code harder to read.
	//	So let's define them as implicitly unwrapped optionals.
	//	I'm slightly uncomfortable using implicitly unwrapped optionals
	//	-- I hadn't intended to use them at all, for run-time safety --
	//	but in this case all we need to do to avoid problems is make sure
	//	that our own init() function provides non-nil values for these
	//	instances variables before returning.
	
	var itsRGBRenderPipelineState: MTLRenderPipelineState!
	var itsRGBBlendRenderPipelineState: MTLRenderPipelineState!
	var itsHSVRenderPipelineState: MTLRenderPipelineState!
	var itsHSVBlendRenderPipelineState: MTLRenderPipelineState!
	var itsTexRenderPipelineState: MTLRenderPipelineState!
	var itsTexBlendRenderPipelineState: MTLRenderPipelineState!

	var itsMakeGridTextureComputePipelineState: MTLComputePipelineState!

	var itsDepthStencilState: MTLDepthStencilState!

	var itsNodeMesh:       GeometryGamesMeshBuffers<Draw4DVertexRGB, Draw4DMeshIndexType>!
	var itsTubeMesh:       GeometryGamesMeshBuffers<Draw4DVertexHSV, Draw4DMeshIndexType>!
	var itsBoxMesh:        GeometryGamesMeshBuffers<Draw4DVertexTex, Draw4DMeshIndexType>!
	var itsGuidelineMesh:  GeometryGamesMeshBuffers<Draw4DVertexHSV, Draw4DMeshIndexType>!
	var itsGuideplaneMesh: GeometryGamesMeshBuffers<Draw4DVertexRGB, Draw4DMeshIndexType>!

	var itsNodeInstancesBufferPool: GeometryGamesBufferPool!
	var itsTubeInstancesBufferPool: GeometryGamesBufferPool!
	var itsBoxInstanceBufferPool: GeometryGamesBufferPool!
	var itsGuidelineInstancesBufferPool: GeometryGamesBufferPool!
	var itsGuideplaneInstancesBufferPool: GeometryGamesBufferPool!

	var itsGridTexture: MTLTexture!

	var itsSamplerState: MTLSamplerState!

		
	struct Draw4DVertexRGB {
		var pos: SIMD3<Float32>	//	position (x,y,z)
		var nor: SIMD3<Float16>	//	unit-length normal vector (nx, ny, nz)

		init(
			pos: SIMD3<Float32>,
			nor: SIMD3<Float16>
		) {
			self.pos = pos
			self.nor = nor
		}
		
		//	This init() takes a nested set of tuples as its sole argument,
		//	so we can write the contents of a vertex buffer as succinctly as possible.
		init( _ v: (
			_ : (_ : Float32, _ : Float32, _ : Float32),
			_ : (_ : Float16, _ : Float16, _ : Float16))
		) {
			pos = SIMD3<Float32>  (v.0.0, v.0.1, v.0.2)
			nor = SIMD3<Float16>(v.1.0, v.1.1, v.1.2)
		}
	}

	struct Draw4DVertexHSV {
		var pos: SIMD3<Float32>	//	position (x,y,z)
		var nor: SIMD3<Float16>	//	unit-length normal vector (nx, ny, nz)
		var wgt: Float16		//	weight ∈ {0.0, 1.0} used to select between the two tube endpoint colors

		init(
			pos: SIMD3<Float32>,
			nor: SIMD3<Float16>,
			wgt: Float16
		) {
			self.pos = pos
			self.nor = nor
			self.wgt = wgt
		}
		
		//	This init() takes a nested set of tuples as its sole argument,
		//	so we can write the contents of a vertex buffer as succinctly as possible.
		init( _ v: (
			_ : (_ : Float32, _ : Float32, _ : Float32),
			_ : (_ : Float16, _ : Float16, _ : Float16),
			_ : Float16)
		) {
			pos = SIMD3<Float32>(v.0.0, v.0.1, v.0.2)
			nor = SIMD3<Float16>(v.1.0, v.1.1, v.1.2)
			wgt = v.2
		}
	}

	struct Draw4DVertexTex {
		var pos: SIMD3<Float32>	//	position (x,y,z)
		var nor: SIMD3<Float16>	//	unit-length normal vector (nx, ny, nz)
		var tex: SIMD2<Float32>	//	texture coordinates (u,v)
		
		//	This init() takes a nested set of tuples as its sole argument,
		//	so we can write the contents of a vertex buffer as succinctly as possible.
		init( _ v: (
			_ : (_ : Float32, _ : Float32, _ : Float32),
			_ : (_ : Float16, _ : Float16, _ : Float16),
			_ : (_ : Float32, _ : Float32))
		) {
			pos = SIMD3<Float32>(v.0.0, v.0.1, v.0.2)
			nor = SIMD3<Float16>(v.1.0, v.1.1, v.1.2)
			tex = SIMD2<Float32>(v.2.0, v.2.1)
		}
	}
	

	let itsNumGuidelines = 4
	let itsNumGuideplanes = 3

	
	init?(
		isDrawingThumbnail: Bool = false
	) {

		//	Specify clear-color values in linear extended-range sRGB.
		let theClearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)

		super.init(
			wantsMultisampling: true,
			wantsDepthBuffer: true,
			clearColor: theClearColor,
			isDrawingThumbnail: isDrawingThumbnail)

		if !(
			setUpPipelineStates()
		 && setUpDepthStencilState()
		 && setUpMeshes()
		 && setUpInflightBufferPools()
		 && setUpTextures()
		 && setUpSamplerStates()
		) {
			assertionFailure("Unable to set up Metal resources")
			return nil
		}
	}

	func setUpPipelineStates() -> Bool {

		guard let theGPUFunctionLibrary = itsDevice.makeDefaultLibrary() else {
			assertionFailure("Failed to create default GPU function library")
			return false
		}
		
		//	On iOS devices (and Apple Silicon Macs) with wide-color displays,
		//	the GPU functions will interpret all given colors as linear Display P3,
		//	and convert them to linear Extended-Range sRGB as Metal requires on iOS.
		var theWideColorConversionIsNeeded = gMainScreenSupportsP3
		let theCompileTimeConstants = MTLFunctionConstantValues()
		theCompileTimeConstants.setConstantValue(
									&theWideColorConversionIsNeeded,
									type: .bool,
									withName: "gUseWideColor")

		let theGPUVertexFunctionRGB: MTLFunction
		do {
			theGPUVertexFunctionRGB = try theGPUFunctionLibrary.makeFunction(
				name: "Draw4DVertexFunctionRGB", constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("setUpPipelineStates error: " + error.localizedDescription)
			return false
		}
		let theGPUFragmentFunctionRGB: MTLFunction
		do {
			theGPUFragmentFunctionRGB = try theGPUFunctionLibrary.makeFunction(
				name: "Draw4DFragmentFunctionRGB", constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("setUpPipelineStates error: " + error.localizedDescription)
			return false
		}
		
		let theGPUVertexFunctionHSV: MTLFunction
		do {
			theGPUVertexFunctionHSV = try theGPUFunctionLibrary.makeFunction(
				name: "Draw4DVertexFunctionHSV", constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("setUpPipelineStates error: " + error.localizedDescription)
			return false
		}
		let theGPUFragmentFunctionHSV: MTLFunction
		do {
			theGPUFragmentFunctionHSV = try theGPUFunctionLibrary.makeFunction(
				name: "Draw4DFragmentFunctionHSV", constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("setUpPipelineStates error: " + error.localizedDescription)
			return false
		}

		let theGPUVertexFunctionTex: MTLFunction
		do {
			theGPUVertexFunctionTex = try theGPUFunctionLibrary.makeFunction(
				name: "Draw4DVertexFunctionTex", constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("setUpPipelineStates error: " + error.localizedDescription)
			return false
		}
		let theGPUFragmentFunctionTex: MTLFunction
		do {
			theGPUFragmentFunctionTex = try theGPUFunctionLibrary.makeFunction(
				name: "Draw4DFragmentFunctionTex", constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("setUpPipelineStates error: " + error.localizedDescription)
			return false
		}
		
		guard let theGPUComputeFunctionMakeGridTexture = theGPUFunctionLibrary.makeFunction(
				name: "Draw4DComputeFunctionMakeGridTexture") else {
			assertionFailure("Failed to load Draw4DComputeFunctionMakeGridTexture")
			return false
		}

		
		//	Create MTLRenderPipelineStates
		
		//	Note:
		//	It's not clear whether makeRenderPipelineState keep references
		//	to thePipelineDescriptor and to the theVertexDescriptor that it contains,
		//	or whether it merely copies the data they contain and then forgets
		//	the objects themselves.  Probably the latter, in which case
		//	we could shorten the following code quite a bit, but better to play it safe
		//	and not modify any objects after we've submitted references to them.
		//	Instead we'll "start fresh" for each pipeline state.
		//
		//	A MTLVertexDescriptor describes the vertex attributes
		//	that the GPU sends along to each vertex.  Each vertex attribute
		//	may be a 1-, 2-, 3- or 4-component vector.
		var theVertexDescriptor: MTLVertexDescriptor
		var thePipelineDescriptor: MTLRenderPipelineDescriptor


		//	RGB
		
		theVertexDescriptor = MTLVertexDescriptor()
		
			//	Say where to find each attribute.

		guard let thePosOffsetRGB = MemoryLayout.offset(of: \Draw4DVertexRGB.pos) else {
			assertionFailure("Failed to find offset of Draw4DVertexRGB.pos")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributePosition].format = MTLVertexFormat.float3
		theVertexDescriptor.attributes[VertexAttributePosition].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributePosition].offset = thePosOffsetRGB

		guard let theNorOffsetRGB = MemoryLayout.offset(of: \Draw4DVertexRGB.nor) else {
			assertionFailure("Failed to find offset of Draw4DVertexRGB.nor")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributeNormal].format = MTLVertexFormat.half3
		theVertexDescriptor.attributes[VertexAttributeNormal].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeNormal].offset = theNorOffsetRGB

			//	Say how to step through each buffer.

		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stepFunction = MTLVertexStepFunction.perVertex
		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stride = MemoryLayout<Draw4DVertexRGB>.stride

		//	Create a MTLRenderPipelineDescriptor.
		
		thePipelineDescriptor = MTLRenderPipelineDescriptor()
		thePipelineDescriptor.label = "4D Draw render pipeline with RGB but no blending"
		thePipelineDescriptor.rasterSampleCount = itsSampleCount
		thePipelineDescriptor.vertexFunction = theGPUVertexFunctionRGB
		thePipelineDescriptor.fragmentFunction = theGPUFragmentFunctionRGB
		thePipelineDescriptor.vertexDescriptor = theVertexDescriptor
		thePipelineDescriptor.colorAttachments[0].pixelFormat = itsColorPixelFormat
		thePipelineDescriptor.depthAttachmentPixelFormat = itsDepthPixelFormat
		thePipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat.invalid

		do {
			itsRGBRenderPipelineState = try itsDevice.makeRenderPipelineState(descriptor: thePipelineDescriptor)	//	returns non-nil
		} catch {
			assertionFailure("Couldn't create itsRGBRenderPipelineState: \(error)")
			return false
		}

		
		//	RGB with alpha blending
		
		theVertexDescriptor = MTLVertexDescriptor()
		
			//	Say where to find each attribute.

		theVertexDescriptor.attributes[VertexAttributePosition].format = MTLVertexFormat.float3
		theVertexDescriptor.attributes[VertexAttributePosition].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributePosition].offset = thePosOffsetRGB

		theVertexDescriptor.attributes[VertexAttributeNormal].format = MTLVertexFormat.half3
		theVertexDescriptor.attributes[VertexAttributeNormal].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeNormal].offset = theNorOffsetRGB

			//	Say how to step through each buffer.

		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stepFunction = MTLVertexStepFunction.perVertex
		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stride = MemoryLayout<Draw4DVertexRGB>.stride

		//	Create a MTLRenderPipelineDescriptor and attach theVertexDescriptor
		//	and (if present) theDepthStencilDescriptor.
		
		thePipelineDescriptor = MTLRenderPipelineDescriptor()
		thePipelineDescriptor.label = "4D Draw render pipeline with RGB and blending"
		thePipelineDescriptor.rasterSampleCount = itsSampleCount
		thePipelineDescriptor.vertexFunction = theGPUVertexFunctionRGB
		thePipelineDescriptor.fragmentFunction = theGPUFragmentFunctionRGB
		thePipelineDescriptor.vertexDescriptor = theVertexDescriptor
		thePipelineDescriptor.colorAttachments[0].pixelFormat = itsColorPixelFormat
		thePipelineDescriptor.depthAttachmentPixelFormat = itsDepthPixelFormat
		thePipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat.invalid

		//	See comments on alpha blending above
		thePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
		thePipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add
		thePipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add
		thePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.one
		thePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.one
		thePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha
		thePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha

		do {
			itsRGBBlendRenderPipelineState = try itsDevice.makeRenderPipelineState(descriptor: thePipelineDescriptor)	//	returns non-nil
		} catch {
			assertionFailure("Couldn't create itsRGBBlendRenderPipelineState: \(error)")
			return false
		}

		
		//	HSV
		
		theVertexDescriptor = MTLVertexDescriptor()
		
			//	Say where to find each attribute.

		guard let thePosOffsetHSV = MemoryLayout.offset(of: \Draw4DVertexHSV.pos) else {
			assertionFailure("Failed to find offset of Draw4DVertexHSV.pos")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributePosition].format = MTLVertexFormat.float3
		theVertexDescriptor.attributes[VertexAttributePosition].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributePosition].offset = thePosOffsetHSV

		guard let theNorOffsetHSV = MemoryLayout.offset(of: \Draw4DVertexHSV.nor) else {
			assertionFailure("Failed to find offset of Draw4DVertexHSV.nor")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributeNormal].format = MTLVertexFormat.half3
		theVertexDescriptor.attributes[VertexAttributeNormal].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeNormal].offset = theNorOffsetHSV

		guard let theWgtOffsetHSV = MemoryLayout.offset(of: \Draw4DVertexHSV.wgt) else {
			assertionFailure("Failed to find offset of Draw4DVertexHSV.wgt")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributeMisc].format = MTLVertexFormat.half
		theVertexDescriptor.attributes[VertexAttributeMisc].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeMisc].offset = theWgtOffsetHSV

			//	Say how to step through each buffer.

		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stepFunction = MTLVertexStepFunction.perVertex
		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stride = MemoryLayout<Draw4DVertexHSV>.stride

		//	Create a MTLRenderPipelineDescriptor and attach theVertexDescriptor
		//	and (if present) theDepthStencilDescriptor.
		
		thePipelineDescriptor = MTLRenderPipelineDescriptor()
		thePipelineDescriptor.label = "4D Draw render pipeline with HSV but no blending"
		thePipelineDescriptor.rasterSampleCount = itsSampleCount
		thePipelineDescriptor.vertexFunction = theGPUVertexFunctionHSV
		thePipelineDescriptor.fragmentFunction = theGPUFragmentFunctionHSV
		thePipelineDescriptor.vertexDescriptor = theVertexDescriptor
		thePipelineDescriptor.colorAttachments[0].pixelFormat = itsColorPixelFormat
		thePipelineDescriptor.depthAttachmentPixelFormat = itsDepthPixelFormat
		thePipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat.invalid

		do {
			itsHSVRenderPipelineState = try itsDevice.makeRenderPipelineState(descriptor: thePipelineDescriptor)	//	returns non-nil
		} catch {
			assertionFailure("Couldn't create itsHSVRenderPipelineState: \(error)")
			return false
		}

		
		//	HSV with alpha blending
		
		theVertexDescriptor = MTLVertexDescriptor()
		
			//	Say where to find each attribute.

		theVertexDescriptor.attributes[VertexAttributePosition].format = MTLVertexFormat.float3
		theVertexDescriptor.attributes[VertexAttributePosition].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributePosition].offset = thePosOffsetHSV

		theVertexDescriptor.attributes[VertexAttributeNormal].format = MTLVertexFormat.half3
		theVertexDescriptor.attributes[VertexAttributeNormal].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeNormal].offset = theNorOffsetHSV

		theVertexDescriptor.attributes[VertexAttributeMisc].format = MTLVertexFormat.half
		theVertexDescriptor.attributes[VertexAttributeMisc].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeMisc].offset = theWgtOffsetHSV

			//	Say how to step through each buffer.

		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stepFunction = MTLVertexStepFunction.perVertex
		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stride = MemoryLayout<Draw4DVertexHSV>.stride

		//	Create a MTLRenderPipelineDescriptor and attach theVertexDescriptor
		//	and (if present) theDepthStencilDescriptor.
		
		thePipelineDescriptor = MTLRenderPipelineDescriptor()
		thePipelineDescriptor.label = "4D Draw render pipeline with HSV and blending"
		thePipelineDescriptor.rasterSampleCount = itsSampleCount
		thePipelineDescriptor.vertexFunction = theGPUVertexFunctionHSV
		thePipelineDescriptor.fragmentFunction = theGPUFragmentFunctionHSV
		thePipelineDescriptor.vertexDescriptor = theVertexDescriptor
		thePipelineDescriptor.colorAttachments[0].pixelFormat = itsColorPixelFormat
		thePipelineDescriptor.depthAttachmentPixelFormat = itsDepthPixelFormat
		thePipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat.invalid

		//	See comments on alpha blending above
		thePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
		thePipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add
		thePipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add
		thePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.one
		thePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.one
		thePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha
		thePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha

		do {
			itsHSVBlendRenderPipelineState = try itsDevice.makeRenderPipelineState(descriptor: thePipelineDescriptor)	//	returns non-nil
		} catch {
			assertionFailure("Couldn't create itsHSVBlendRenderPipelineState: \(error)")
			return false
		}

		
		//	textured
	 
		theVertexDescriptor = MTLVertexDescriptor()
		
			//	Say where to find each attribute.

		guard let thePosOffsetTex = MemoryLayout.offset(of: \Draw4DVertexTex.pos) else {
			assertionFailure("Failed to find offset of Draw4DVertexTex.pos")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributePosition].format = MTLVertexFormat.float3
		theVertexDescriptor.attributes[VertexAttributePosition].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributePosition].offset = thePosOffsetTex

		guard let theNorOffsetTex = MemoryLayout.offset(of: \Draw4DVertexTex.nor) else {
			assertionFailure("Failed to find offset of Draw4DVertexTex.nor")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributeNormal].format = MTLVertexFormat.half3
		theVertexDescriptor.attributes[VertexAttributeNormal].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeNormal].offset = theNorOffsetTex

		guard let theTexOffsetTex = MemoryLayout.offset(of: \Draw4DVertexTex.tex) else {
			assertionFailure("Failed to find offset of Draw4DVertexTex.tex")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributeMisc].format = MTLVertexFormat.float2
		theVertexDescriptor.attributes[VertexAttributeMisc].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeMisc].offset = theTexOffsetTex

			//	Say how to step through each buffer.

		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stepFunction = MTLVertexStepFunction.perVertex
		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stride = MemoryLayout<Draw4DVertexTex>.stride

		//	Create a MTLRenderPipelineDescriptor and attach theVertexDescriptor
		//	and (if present) theDepthStencilDescriptor.
		
		thePipelineDescriptor = MTLRenderPipelineDescriptor()
		thePipelineDescriptor.label = "4D Draw render pipeline with texture but no blending"
		thePipelineDescriptor.rasterSampleCount = itsSampleCount
		thePipelineDescriptor.vertexFunction = theGPUVertexFunctionTex
		thePipelineDescriptor.fragmentFunction = theGPUFragmentFunctionTex
		thePipelineDescriptor.vertexDescriptor = theVertexDescriptor
		thePipelineDescriptor.colorAttachments[0].pixelFormat = itsColorPixelFormat
		thePipelineDescriptor.depthAttachmentPixelFormat = itsDepthPixelFormat
		thePipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat.invalid

		do {
			itsTexRenderPipelineState = try itsDevice.makeRenderPipelineState(descriptor: thePipelineDescriptor)	//	returns non-nil
		} catch {
			assertionFailure("Couldn't create itsTexRenderPipelineState: \(error)")
			return false
		}


		//	textured with alpha blending (currently unused)
	 
		theVertexDescriptor = MTLVertexDescriptor()
		
			//	Say where to find each attribute.

		theVertexDescriptor.attributes[VertexAttributePosition].format = MTLVertexFormat.float3
		theVertexDescriptor.attributes[VertexAttributePosition].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributePosition].offset = thePosOffsetTex

		theVertexDescriptor.attributes[VertexAttributeNormal].format = MTLVertexFormat.half3
		theVertexDescriptor.attributes[VertexAttributeNormal].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeNormal].offset = theNorOffsetTex

		theVertexDescriptor.attributes[VertexAttributeMisc].format = MTLVertexFormat.float2
		theVertexDescriptor.attributes[VertexAttributeMisc].bufferIndex = BufferIndexVFVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeMisc].offset = theTexOffsetTex

			//	Say how to step through each buffer.

		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stepFunction = MTLVertexStepFunction.perVertex
		theVertexDescriptor.layouts[BufferIndexVFVertexAttributes].stride = MemoryLayout<Draw4DVertexTex>.stride

		//	Create a MTLRenderPipelineDescriptor and attach theVertexDescriptor
		//	and (if present) theDepthStencilDescriptor.
		
		thePipelineDescriptor = MTLRenderPipelineDescriptor()
		thePipelineDescriptor.label = "4D Draw render pipeline with texture and blending"
		thePipelineDescriptor.rasterSampleCount = itsSampleCount
		thePipelineDescriptor.vertexFunction = theGPUVertexFunctionTex
		thePipelineDescriptor.fragmentFunction = theGPUFragmentFunctionTex
		thePipelineDescriptor.vertexDescriptor = theVertexDescriptor
		thePipelineDescriptor.colorAttachments[0].pixelFormat = itsColorPixelFormat
		thePipelineDescriptor.depthAttachmentPixelFormat = itsDepthPixelFormat
		thePipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat.invalid

		//	See comments on alpha blending above
		thePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
		thePipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add
		thePipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add
		thePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.one
		thePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.one
		thePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha
		thePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha

		do {
			itsTexBlendRenderPipelineState = try itsDevice.makeRenderPipelineState(descriptor: thePipelineDescriptor)	//	returns non-nil
		} catch {
			assertionFailure("Couldn't create itsTexBlendRenderPipelineState: \(error)")
			return false
		}

		
		//	Create MTLComputePipelineState

		do {
			itsMakeGridTextureComputePipelineState = try itsDevice.makeComputePipelineState(
				function: theGPUComputeFunctionMakeGridTexture)
		} catch {
			assertionFailure("Couldn't create itsMakeGridTextureComputePipelineState: \(error)")
			return false
		}

		
		return true
	}
	
	func setUpDepthStencilState() -> Bool {

		//	init() has already checked that itsDepthPixelFormat != MTLPixelFormat.invalid
		
		let theDepthStencilDescriptor = MTLDepthStencilDescriptor()
		theDepthStencilDescriptor.depthCompareFunction = MTLCompareFunction.less
		theDepthStencilDescriptor.isDepthWriteEnabled = true

		itsDepthStencilState = itsDevice.makeDepthStencilState(
									descriptor: theDepthStencilDescriptor)
		if itsDepthStencilState == nil {
			assertionFailure("Couldn't create itsDepthStencilState")
			return false
		}
		
		return true
	}
	
	func setUpMeshes() -> Bool {

		itsNodeMesh = makeNodeMesh()
		itsTubeMesh = makeTubeMesh()
		itsBoxMesh = makeBoxMesh()
		itsGuidelineMesh = makeGuidelineMesh()
		itsGuideplaneMesh = makeGuideplaneMesh()

		return true
	}
	
	func setUpInflightBufferPools() -> Bool {

		//	Each node instances buffer will contain an array of Draw4DInstanceDataRGB.
		let theOriginalNodeCount = 1
		itsNodeInstancesBufferPool = GeometryGamesBufferPool(
			device: itsDevice,
			initialBufferSize: theOriginalNodeCount * MemoryLayout<Draw4DInstanceDataRGB>.stride,
			storageMode: .storageModeShared,
			bufferLabel: "Node instances buffer")

		//	Each tube instances buffer will contain an array of Draw4DInstanceDataHSV.
		let theOriginalTubeCount = 1
		itsTubeInstancesBufferPool = GeometryGamesBufferPool(
			device: itsDevice,
			initialBufferSize: theOriginalTubeCount * MemoryLayout<Draw4DInstanceDataHSV>.stride,
			storageMode: .storageModeShared,
			bufferLabel: "Tube instances buffer")

		//	Each box instance buffer will contain a single Draw4DInstanceDataTex.
		itsBoxInstanceBufferPool = GeometryGamesBufferPool(
			device: itsDevice,
			initialBufferSize: MemoryLayout<Draw4DInstanceDataTex>.stride,
			storageMode: .storageModeShared,
			bufferLabel: "Box instance buffer")
		
		//	Each guideline instances buffer will contain
		//	a 4-element array of Draw4DInstanceDataHSV.
		itsGuidelineInstancesBufferPool = GeometryGamesBufferPool(
			device: itsDevice,
			initialBufferSize: itsNumGuidelines *
				MemoryLayout<Draw4DInstanceDataHSV>.stride,
			storageMode: .storageModeShared,
			bufferLabel: "Guideline instances buffer")
		
		//	Each guideplane instances buffer will contain
		//	a 3-element array of Draw4DInstanceDataRGB.
		itsGuideplaneInstancesBufferPool = GeometryGamesBufferPool(
			device: itsDevice,
			initialBufferSize: itsNumGuideplanes *
				MemoryLayout<Draw4DInstanceDataRGB>.stride,
			storageMode: .storageModeShared,
			bufferLabel: "Guideplane instances buffer")

		return true
	}
	
	func setUpTextures() -> Bool {
		
		itsGridTexture = makeGridTexture()
		if itsGridTexture == nil {
			assertionFailure("couldn't create itsGridTexture")
			return false
		}
		
		return true
	}
	
	func setUpSamplerStates() -> Bool {
		
		itsSamplerState = makeSamplerState(mode: .repeat, anisotropic: true)
		if itsSamplerState == nil {
			assertionFailure("Couldn't create itsSamplerState")
			return false
		}
		
		return true
	}

	
	// MARK: -
	// MARK: rendering

	//	GeometryGamesRenderer (our superclass) provides the functions
	//
	//		render()
	//			for standard onscreen rendering
	//
	//		createOffscreenImage()
	//			for CopyImage and SaveImage
	//
	//	Those two functions handle the app-independent parts
	//	of rendering an image, but for the app-dependent parts
	//	they call encodeCommands(), which we override here
	//	to provide the app-specific content.

	override func encodeCommands(
		modelData: Draw4DDocument,
		commandBuffer: MTLCommandBuffer,
		renderPassDescriptor: MTLRenderPassDescriptor,
		frameWidth: Int,
		frameHeight: Int,
		transparentBackground: Bool,	//	Only KaleidoPaint and KaleidoTile need to know
										//	transparentBackground while encoding commands.
		extraRenderFlag: Bool?,
		quality: GeometryGamesImageQuality
	) {

		var theUniformData = prepareUniformData(
				modelData: modelData,
				frameWidth: frameWidth,
				frameHeight: frameHeight)

		let (theNodeInstancesBuffer, theNumNodes) = prepareNodeInstances(
				modelData: modelData,
				commandBuffer: commandBuffer)

		let (theTubeInstancesBuffer, theNumTubes) = prepareTubeInstances(
				modelData: modelData,
				commandBuffer: commandBuffer)
				
		let theOptionalBoxInstanceBuffer = prepareBoxInstance(
				modelData: modelData,
				commandBuffer: commandBuffer)

		let theOptionalGuidelineInstancesBuffer = prepareGuidelineInstances(
				modelData: modelData,
				commandBuffer: commandBuffer)

		let theOptionalGuideplaneInstancesBuffer = prepareGuideplaneInstances(
				modelData: modelData,
				commandBuffer: commandBuffer)

		if let theCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) {
					
			theCommandEncoder.label = "4D Draw render encoder"

			theCommandEncoder.setDepthStencilState(itsDepthStencilState)
			theCommandEncoder.setCullMode(MTLCullMode.back)
			theCommandEncoder.setFrontFacing(MTLWinding.clockwise)	//	the default
			theCommandEncoder.setVertexBytes(
								&theUniformData,
								length: MemoryLayout.size(ofValue: theUniformData),
								index: BufferIndexVFUniforms)

			//	nodes (monochrome RGB)
			if theNumNodes > 0 {
				theCommandEncoder.pushDebugGroup("Draw nodes")
				theCommandEncoder.setRenderPipelineState(itsRGBRenderPipelineState)
				theCommandEncoder.setVertexBuffer(
									theNodeInstancesBuffer,
									offset: 0,
									index: BufferIndexVFInstanceData)
				theCommandEncoder.setVertexBuffer(
									itsNodeMesh.vertexBuffer,
									offset:	0,
									index: BufferIndexVFVertexAttributes)
				theCommandEncoder.drawIndexedPrimitives(
									type: .triangle,
									indexCount: itsNodeMesh.indexCount,
									indexType: itsNodeMesh.mtlIndexType,
									indexBuffer: itsNodeMesh.indexBuffer,
									indexBufferOffset: 0,
									instanceCount: theNumNodes)
				theCommandEncoder.popDebugGroup()
			}

			//	tubes (rainbow HSV)
			if theNumTubes > 0 {
				theCommandEncoder.pushDebugGroup("Draw tubes")
				theCommandEncoder.setRenderPipelineState(itsHSVRenderPipelineState)
				theCommandEncoder.setVertexBuffer(
									theTubeInstancesBuffer,
									offset: 0,
									index: BufferIndexVFInstanceData)
				theCommandEncoder.setVertexBuffer(
									itsTubeMesh.vertexBuffer,
									offset:	0,
									index: BufferIndexVFVertexAttributes)
				theCommandEncoder.drawIndexedPrimitives(
									type: .triangle,
									indexCount: itsTubeMesh.indexCount,
									indexType: itsTubeMesh.mtlIndexType,
									indexBuffer: itsTubeMesh.indexBuffer,
									indexBufferOffset: 0,
									instanceCount: theNumTubes)
				theCommandEncoder.popDebugGroup()
			}

			//	Is the box visible?
			if let theBoxInstanceBuffer = theOptionalBoxInstanceBuffer {
			
				theCommandEncoder.pushDebugGroup("Draw box")
				theCommandEncoder.setRenderPipelineState(itsTexRenderPipelineState)
				theCommandEncoder.setVertexBuffer(
									theBoxInstanceBuffer,
									offset: 0,
									index: BufferIndexVFInstanceData)
				theCommandEncoder.setVertexBuffer(
									itsBoxMesh.vertexBuffer,
									offset:	0,
									index: BufferIndexVFVertexAttributes)
				theCommandEncoder.setFragmentTexture(
									itsGridTexture,
									index: TextureIndexFFPrimary)
				theCommandEncoder.setFragmentSamplerState(
									itsSamplerState,
									index: SamplerIndexFFPrimary)
				theCommandEncoder.drawIndexedPrimitives(
									type: .triangle,
									indexCount: itsBoxMesh.indexCount,
									indexType: itsBoxMesh.mtlIndexType,
									indexBuffer: itsBoxMesh.indexBuffer,
									indexBufferOffset: 0,
									instanceCount: 1)
				theCommandEncoder.popDebugGroup()
			}

			//	Are the guidelines visible?
			if let theGuidelineInstancesBuffer = theOptionalGuidelineInstancesBuffer {

				theCommandEncoder.pushDebugGroup("Draw guidelines")
				theCommandEncoder.setRenderPipelineState(itsHSVRenderPipelineState)
				theCommandEncoder.setVertexBuffer(
									theGuidelineInstancesBuffer,
									offset: 0,
									index: BufferIndexVFInstanceData)
				theCommandEncoder.setVertexBuffer(
									itsGuidelineMesh.vertexBuffer,
									offset:	0,
									index: BufferIndexVFVertexAttributes)
				theCommandEncoder.drawIndexedPrimitives(
									type: .triangle,
									indexCount: itsGuidelineMesh.indexCount,
									indexType: itsGuidelineMesh.mtlIndexType,
									indexBuffer: itsGuidelineMesh.indexBuffer,
									indexBufferOffset: 0,
									instanceCount: itsNumGuidelines)
				theCommandEncoder.popDebugGroup()
			}

			//	As always, rendering the semi-transparent content
			//	*after* all opaque content gives the best-looking result.
			//	For example, an opaque node will remain visible
			//	even when a transparent wall sits in front of it.
			//	For more details, see the section title Transparency in
			//
			//		https://blog.imaginationtech.com/the-dr-in-tbdr-deferred-rendering-in-rogue/
			//
			//	The guideplanes are partially transparent, so draw them last.
			
			//	Are the guideplanes visible?
			if let theGuideplaneInstancesBuffer = theOptionalGuideplaneInstancesBuffer {
			
				theCommandEncoder.pushDebugGroup("Draw guideplanes")
				theCommandEncoder.setRenderPipelineState(itsRGBBlendRenderPipelineState)
				theCommandEncoder.setVertexBuffer(
									theGuideplaneInstancesBuffer,
									offset: 0,
									index: BufferIndexVFInstanceData)
				theCommandEncoder.setVertexBuffer(
									itsGuideplaneMesh.vertexBuffer,
									offset:	0,
									index: BufferIndexVFVertexAttributes)
				theCommandEncoder.drawIndexedPrimitives(
									type: .triangle,
									indexCount: itsGuideplaneMesh.indexCount,
									indexType: itsGuideplaneMesh.mtlIndexType,
									indexBuffer: itsGuideplaneMesh.indexBuffer,
									indexBufferOffset: 0,
									instanceCount: itsNumGuideplanes)
				theCommandEncoder.popDebugGroup()
			}

			//	All done!
			theCommandEncoder.endEncoding()
		}
		
	}

	
	// MARK: -
	// MARK: uniforms

	func prepareUniformData(
		modelData: Draw4DDocument,
		frameWidth: Int,
		frameHeight: Int
	) -> Draw4DUniformData {

		//	The node and tube instances take into account itsOrientation,
		//	so here we need only set theProjectionMatrix.
		let theProjectionMatrix = makeProjectionMatrix(
									frameWidth: Double(frameWidth),
									frameHeight: Double(frameHeight))

		let theUniformData = Draw4DUniformData(
			itsProjectionMatrix: convertDouble4x4toFloat4x4(theProjectionMatrix) )

		return theUniformData
	}

	func makeProjectionMatrix(
		frameWidth: Double,	//	typically in pixels or points
		frameHeight: Double	//	typically in pixels or points
	) -> simd_double4x4 {	//	returns the projection matrix
	
		if frameWidth <= 0.0 || frameHeight <= 0.0 {
			assertionFailure("nonpositive-size image received")
			return matrix_identity_double4x4
		}
		
		let theIntrinsicUnitsPerPixelOrPoint = intrinsicUnitsPerPixelOrPoint(
													viewWidth: frameWidth,
													viewHeight: frameHeight)
		
		//	Compute the frame's half-width and half-height in intrinsic units.
		let w = 0.5 * frameWidth  * theIntrinsicUnitsPerPixelOrPoint	//	half width
		let h = 0.5 * frameHeight * theIntrinsicUnitsPerPixelOrPoint	//	half height

		//	The observer's eye sits (gEyeDistance - gBoxSize) intrinsic units
		//	from the center of the display.
		let d = gEyeDistance - gBoxSize

		//	In eye coordinates, the scene will be centered at (0, 0, gEyeDistance),
		//	so let the clipping range run from gEyeDistance - 2*gBoxSize
		//	to gEyeDistance + 2*gBoxSize to ensure that the whole box (and a little more)
		//	is visible no matter how it's rotated.
		let n = gEyeDistance - 2.0 * gBoxSize	//	Don't let n get too close to zero.  n ≥ 0.01 is safe.
		let f = gEyeDistance + 2.0 * gBoxSize
		precondition(
			n >= 0.01,
			"Near clip plane is too close.")

		precondition(
			w > 0.0 && h > 0.0 && d > 0.0 && n > 0.0 && f > n,
			"invalid projection parameters")

		//	The eight points
		//
		//			(±n*(w/d), ±n*(h/d), n, 1) and
		//			(±f*(w/d), ±f*(h/d), f, 1)
		//
		//	define the view frustum.  More precisely,
		//	because the GPU works in 4D homogeneous coordinates,
		//	it's really a set of eight rays, from the origin (0,0,0,0)
		//	through each of those eight points, that defines
		//	the view frustum as a "hyper-wedge" in 4D space.
		//
		//	Because the GPU works in homogenous coordinates,
		//	we may multiply each point by any scalar constant we like,
		//	without changing the ray that it represents.
		//	So let divide each of those eight points through
		//	by its own z coordinate, giving
		//
		//			(±w/d, ±h/d, 1, 1/n) and
		//			(±w/d, ±h/d, 1, 1/f)
		//
		//	Geometrically, these points define the intersection
		//	of the 4D wedge with the hyperplane z = 1.
		//	Conveniently enough, this intersection is a rectangular box!
		//
		//	Our goal is to find a 4×4 matrix that takes this rectangular box
		//	to the standard clipping box with corners at
		//
		//			(±1, ±1, 0, 1) and
		//			(±1, ±1, 1, 1)
		//
		//	To find such a matrix, let's "follow our nose"
		//	and construct it as the product of several factors.
		//
		//		Note:  Unlike in older versions of the Geometry Games
		//		source code, matrices now act using
		//		the right-to-left (matrix)(column vector) convention,
		//		not the left-to-right (row vector)(matrix) convention.]
		//
		//	Factor #1
		//
		//		The quarter turn matrix
		//
		//			1  0  0  0
		//			0  1  0  0
		//			0  0  0 -1
		//			0  0  1  0
		//
		//		takes the eight points
		//			(±w/d, ±h/d, 1, 1/n) and
		//			(±w/d, ±h/d, 1, 1/f)
		//		to
		//			(±w/d, ±h/d, -1/n, 1) and
		//			(±w/d, ±h/d, -1/f, 1)
		//
		//	Factor #2
		//
		//		The xy dilation
		//
		//			d/w  0   0   0
		//			 0  d/h  0   0
		//			 0   0   1   0
		//			 0   0   0   1
		//
		//		takes
		//			(±w/d, ±h/d, -1/n, 1) and
		//			(±w/d, ±h/d, -1/f, 1)
		//		to
		//			(±1, ±1, -1/n, 1) and
		//			(±1, ±1, -1/f, 1)
		//
		//	Factor #3
		//
		//		The z dilation
		//
		//			1  0  0  0
		//			0  1  0  0
		//			0  0  a  0
		//			0  0  0  1
		//
		//		where a = n*f/(f - n), stretches or shrinks
		//		the box to have unit length in the z direction,
		//		taking
		//			(±1, ±1, -1/n, 1) and
		//			(±1, ±1, -1/f, 1)
		//		to
		//			(±1, ±1, -f/(f - n), 1) and
		//			(±1, ±1, -n/(f - n), 1)
		//
		//	Factor #4
		//
		//		The shear
		//
		//			1  0  0  0
		//			0  1  0  0
		//			0  0  1  b
		//			0  0  0  1
		//
		//		where b = f/(f - n), translates the hyperplane w = 1
		//		just the right amount to take
		//
		//			(±1, ±1, -f/(f - n), 1) and
		//			(±1, ±1, -n/(f - n), 1)
		//		to
		//			(±1, ±1, 0, 1) and
		//			(±1, ±1, 1, 1)
		//
		//		which are the vertices of the standard clipping box,
		//		which is exactly where we wanted to end up.
		//
		//	The projection matrix is the product (taken right-to-left!)
		//	of those four factors:
		//
		//			( 1  0  0  0 )( 1  0  0  0 )( d/w  0   0   0 )( 1  0  0  0 )
		//			( 0  1  0  0 )( 0  1  0  0 )(  0  d/h  0   0 )( 0  1  0  0 )
		//			( 0  0  1  b )( 0  0  a  0 )(  0   0   1   0 )( 0  0  0 -1 )
		//			( 0  0  0  1 )( 0  0  0  1 )(  0   0   0   1 )( 0  0  1  0 )
		//		=
		//			( d/w  0   0   0 )
		//			(  0  d/h  0   0 )
		//			(  0   0   b  -a )
		//			(  0   0   1   0 )
		//

		let theRawProjectionMatrix = simd_double4x4(
			SIMD4<Double>(d/w, 0.0,     0.0,    0.0),
			SIMD4<Double>(0.0, d/h,     0.0,    0.0),
			SIMD4<Double>(0.0, 0.0,   f/(f-n),  1.0),
			SIMD4<Double>(0.0, 0.0, -n*f/(f-n), 0.0))

		//	Let theTranslation be the first factor in the projection matrix
		//	rather than the last factor in the view matrix.

		var theTranslation = matrix_identity_double4x4
		theTranslation[3][2] = gEyeDistance
		
		let theProjectionMatrix
			= theRawProjectionMatrix	//	right-to-left matrix composition
			* theTranslation

		return theProjectionMatrix
	}

	
	// MARK: -
	// MARK: nodes

	func makeNodeMesh() -> GeometryGamesMeshBuffers<Draw4DVertexRGB, Draw4DMeshIndexType> {

		let theRefinementLevel = 3	//	could be higher for high-resolution screenshots
		
		//	Create a unit sphere.
		let theUnitSphereMesh: GeometryGamesMesh<GeometryGamesPlainVertex, Draw4DMeshIndexType>
			= unitSphereMesh(
				baseShape: .icosahedron,
				refinementLevel: theRefinementLevel)

		let theNodeVertices = theUnitSphereMesh.itsVertices.map() { v in

			//	Convert from GeometryGamesPlainVertex to Draw4DVertexRGB
			//	while also rescaling the node.
			//
			//		Note:  Draw4DVertexRGB is the same as a GeometryGamesPlainVertex,
			//		but I've left them as distinct types to keep the parallelism
			//		among {Draw4DVertexRGB, Draw4DVertexHSV, Draw4DVertexTex},
			//		and also for future flexibility.
			//
			
			Draw4DVertexRGB(
				pos: Float32(gNodeRadius) * v.pos,
				nor: v.nor)
		}

		let theNodeIndices = theUnitSphereMesh.itsIndices	//	same facet indices

		let theNodeMesh = GeometryGamesMesh(
							vertices: theNodeVertices,
							indices: theNodeIndices)

		let theNodeMeshBuffers = theNodeMesh.makeBuffers(device: itsDevice)

		return theNodeMeshBuffers
	}

	func prepareNodeInstances(
		modelData: Draw4DDocument,
		commandBuffer: MTLCommandBuffer
	) -> (MTLBuffer, Int) {	//	returns (buffer, node count)
		
		let theNodeCount = modelData.itsPoints.count
		
		itsNodeInstancesBufferPool.ensureSize(
			theNodeCount * MemoryLayout<Draw4DInstanceDataRGB>.stride)
		
		let theNodeInstanceBuffer = itsNodeInstancesBufferPool.get(forUseWith: commandBuffer)
		let theNodeInstances = theNodeInstanceBuffer.contents()
			.bindMemory(to: Draw4DInstanceDataRGB.self, capacity: theNodeCount)
		
		let theTransformation = composeTransformation(
			orientation: modelData.itsOrientation,
			animatedRotation: modelData.itsAnimatedRotation)
		
		let theTime = CFAbsoluteTimeGetCurrent()
		let theFlashFactorSlow = 0.5 * (1.0 + cos(gFlashRateSlow * theTime))
		let theFlashFactorFast = 0.5 * (1.0 + cos(gFlashRateFast * theTime))

		for i in 0 ..< theNodeCount {

			let thePoint = modelData.itsPoints[i]

			let (theTransformedPosition, _, theHue) = transformedPositionAndHue(
				position: thePoint.itsPosition,
				transformation: theTransformation)

			var theModelViewMatrix = matrix_identity_float4x4
			theModelViewMatrix[3][0] = Float(theTransformedPosition[0])
			theModelViewMatrix[3][1] = Float(theTransformedPosition[1])
			theModelViewMatrix[3][2] = Float(theTransformedPosition[2])
			theNodeInstances[i].itsModelViewMatrix = theModelViewMatrix
			
			var theSaturation = 1.0
			var theValue = 1.0
			switch modelData.itsTouchMode {
			
			case .neutral, .movePoints, .addPoints, .deleteEdges:
				break
				
			case .deletePoints:
			
				//	Modulate theValue.
				//	This is equivalent to blending the color with black.
				theValue = 0.5 + 0.5*theFlashFactorSlow
				
			case .addEdges:
			
				//	Modulate theSaturation.
				//	This is equivalent to blending the color with white.
				
				if let theSelectedPoint = modelData.itsSelectedPoint {
				
					if thePoint == theSelectedPoint {
					
						//	Let the already selected point flash quickly.
						theSaturation = theFlashFactorFast
						
					} else {	//	thePoint != theSelectedPoint
					
						//	Let each point not yet connected to theSelectedPoint flash slowly.
						if !modelData.pointsAreConnected(thePoint, theSelectedPoint) {
							theSaturation = theFlashFactorSlow
						}
					}
					
				} else {	//	itsSelectedPoint == nil
				
					//	Let each non-saturated point flash slowly.
					if !modelData.pointIsSaturated(thePoint) {
						theSaturation = theFlashFactorSlow
					}
				}
				
			}

			//	The vertex shader will decide whether to interpret
			//	itsRGB as linear Display P3 or linear sRGB.
			theNodeInstances[i].itsRGB = HSVtoRGB(
											hue: theHue,
											saturation: theSaturation,
											value: theValue)
			
			theNodeInstances[i].itsOpacity = Float16(1.0)
		}

		return (theNodeInstanceBuffer, theNodeCount)
	}
		
	
	// MARK: -
	// MARK: tubes

	func makeTubeMesh() -> GeometryGamesMeshBuffers<Draw4DVertexHSV, Draw4DMeshIndexType> {

		let theRefinementLevel = 3	//	could be higher for high-resolution screenshots
		
		//	Create a unit cylinder.
		let theUnitCylinderMesh: GeometryGamesMesh<GeometryGamesPlainVertex, Draw4DMeshIndexType>
			= unitCylinderMesh(
				style: .antiprism,
				refinementLevel: theRefinementLevel)

		let theTubeVertices = theUnitCylinderMesh.itsVertices.map() { v in

			//	Convert from GeometryGamesPlainVertex to Draw4DVertexHSV
			//	while also rescaling the tube.
			
			Draw4DVertexHSV(

				pos: SIMD3<Float32>(
					Float32(gTubeRadius) * v.pos[0],
					Float32(gTubeRadius) * v.pos[1],
					v.pos[2]	//	= ±1.0
				),

				nor: v.nor,

				//	The sign of the z component of v.pos determines
				//	the vertex's "weight" ∈ {0.0, 1.0}, which gets used
				//	to select between the two tube endpoint colors.
				wgt: Float16(v.pos[2] > 0.0 ? 0.0 : 1.0)
			)
		}

		let theTubeIndices = theUnitCylinderMesh.itsIndices	//	same facet indices

		let theTubeMesh = GeometryGamesMesh(
							vertices: theTubeVertices,
							indices: theTubeIndices)

		let theTubeMeshBuffers = theTubeMesh.makeBuffers(device: itsDevice)

		return theTubeMeshBuffers
	}

	func prepareTubeInstances(
		modelData: Draw4DDocument,
		commandBuffer: MTLCommandBuffer
	) -> (MTLBuffer, Int) {	//	returns (buffer, tube count)
		
		let theTubeCount = modelData.itsEdges.count
		
		itsTubeInstancesBufferPool.ensureSize(
			theTubeCount * MemoryLayout<Draw4DInstanceDataHSV>.stride)

		let theTubeInstanceBuffer = itsTubeInstancesBufferPool.get(forUseWith: commandBuffer)
		let theTubeInstances = theTubeInstanceBuffer.contents()
			.bindMemory(to: Draw4DInstanceDataHSV.self, capacity: theTubeCount)

		let theTransformation = composeTransformation(
			orientation: modelData.itsOrientation,
			animatedRotation: modelData.itsAnimatedRotation)

		let theTime = CFAbsoluteTimeGetCurrent()
		let theFlashFactorSlow = Float16(0.5 * (1.0 + cos(gFlashRateSlow * theTime)))

		for i in 0 ..< theTubeCount {

			let (theTransformedStartPosition, _, theStartHue) = transformedPositionAndHue(
					position: modelData.itsEdges[i].itsStart.itsPosition,
					transformation: theTransformation)

			let ( theTransformedEndPosition, _,   theEndHue ) = transformedPositionAndHue(
					position: modelData.itsEdges[i].itsEnd.itsPosition,
					transformation: theTransformation)
			
			let theEdgeVector = theTransformedEndPosition - theTransformedStartPosition
			let theEdgeLength = length(theEdgeVector)
			let theEdgeCenter = 0.5 * (theTransformedStartPosition + theTransformedEndPosition)
			
			//	Stretch the cylinder by a factor of 0.5*theEdgeLength in the z direction.
			let theStretch = simd_double3x3(diagonal: SIMD3<Double>(1.0, 1.0, 0.5 * theEdgeLength))
			
			//	Rotate the cylinder to align with theEdgeVector.
			let theRotation: simd_double3x3
			if theEdgeLength > 0.0001 {
				theRotation = simd_double3x3(simd_quatd(
					from: SIMD3<Double>(0.0, 0.0, 1.0),
					to: normalize(theEdgeVector)))
			} else {	//	theEdgeLength ≈ 0
				//	Don't try to rotate an almost zero-length cylinder.
				theRotation = matrix_identity_double3x3
			}
			let theRotatedStretch = theRotation * theStretch
			
			//	Translate the cylinder to coincide with the edge.
			let theModelViewMatrix = simd_double4x4(
				SIMD4<Double>(theRotatedStretch[0], 0.0),
				SIMD4<Double>(theRotatedStretch[1], 0.0),
				SIMD4<Double>(theRotatedStretch[2], 0.0),
				SIMD4<Double>(theEdgeCenter[0], theEdgeCenter[1], theEdgeCenter[2], 1.0)
			)

			let theSaturation: Float16 = 1.0
			let theValue: Float16
			if modelData.itsTouchMode == .deleteEdges {
			
				//	Modulate theValue.
				//	This is equivalent to blending the color with black.
				theValue = 0.5 + 0.5*theFlashFactorSlow
				
			} else {
			
				theValue = 1.0
			}

			theTubeInstances[i].itsModelViewMatrix = convertDouble4x4toFloat4x4(theModelViewMatrix)
			theTubeInstances[i].itsHSV0 = SIMD3<Float16>(Float16(theStartHue), theSaturation, theValue)
			theTubeInstances[i].itsHSV1 = SIMD3<Float16>(Float16( theEndHue ), theSaturation, theValue)
			theTubeInstances[i].itsOpacity = Float16(1.0)
		}
			
		return (theTubeInstanceBuffer, theTubeCount)
	}
	
	
	// MARK: -
	// MARK: box

	func makeBoxMesh() -> GeometryGamesMeshBuffers<Draw4DVertexTex, Draw4DMeshIndexType> {
		
		//	Draw the box a tiny bit larger than its nominal size,
		//	to avoid z-fighting when then a recent-point plane
		//	coincides with a box wall.
		let theBoxPaddingFactor = 1.001
		let pbs: Float32 = Float32(theBoxPaddingFactor * gBoxSize)	//	"pbs" = "padded box size"
		let mgc: Float32 = Float32(gBoxSize / gGridSpacing)			//	"mgc" = "maximum grid coordinate"
		
		let theBoxVertices: [Draw4DVertexTex] = [

			//	x == -pbs
			Draw4DVertexTex(((-pbs, -pbs, -pbs), (+1.0,  0.0,  0.0), (-mgc, -mgc))),
			Draw4DVertexTex(((-pbs, +pbs, -pbs), (+1.0,  0.0,  0.0), (+mgc, -mgc))),
			Draw4DVertexTex(((-pbs, -pbs, +pbs), (+1.0,  0.0,  0.0), (-mgc, +mgc))),
			Draw4DVertexTex(((-pbs, +pbs, +pbs), (+1.0,  0.0,  0.0), (+mgc, +mgc))),

			//	x == +pbs
			Draw4DVertexTex(((+pbs, -pbs, -pbs), (-1.0,  0.0,  0.0), (-mgc, -mgc))),
			Draw4DVertexTex(((+pbs, -pbs, +pbs), (-1.0,  0.0,  0.0), (+mgc, -mgc))),
			Draw4DVertexTex(((+pbs, +pbs, -pbs), (-1.0,  0.0,  0.0), (-mgc, +mgc))),
			Draw4DVertexTex(((+pbs, +pbs, +pbs), (-1.0,  0.0,  0.0), (+mgc, +mgc))),

			//	y == -pbs
			Draw4DVertexTex(((-pbs, -pbs, -pbs), ( 0.0, +1.0,  0.0), (-mgc, -mgc))),
			Draw4DVertexTex(((-pbs, -pbs, +pbs), ( 0.0, +1.0,  0.0), (+mgc, -mgc))),
			Draw4DVertexTex(((+pbs, -pbs, -pbs), ( 0.0, +1.0,  0.0), (-mgc, +mgc))),
			Draw4DVertexTex(((+pbs, -pbs, +pbs), ( 0.0, +1.0,  0.0), (+mgc, +mgc))),

			//	y == +pbs
			Draw4DVertexTex(((-pbs, +pbs, -pbs), ( 0.0, -1.0,  0.0), (-mgc, -mgc))),
			Draw4DVertexTex(((+pbs, +pbs, -pbs), ( 0.0, -1.0,  0.0), (+mgc, -mgc))),
			Draw4DVertexTex(((-pbs, +pbs, +pbs), ( 0.0, -1.0,  0.0), (-mgc, +mgc))),
			Draw4DVertexTex(((+pbs, +pbs, +pbs), ( 0.0, -1.0,  0.0), (+mgc, +mgc))),

			//	z == -pbs
			Draw4DVertexTex(((-pbs, -pbs, -pbs), ( 0.0,  0.0, +1.0), (-mgc, -mgc))),
			Draw4DVertexTex(((+pbs, -pbs, -pbs), ( 0.0,  0.0, +1.0), (+mgc, -mgc))),
			Draw4DVertexTex(((-pbs, +pbs, -pbs), ( 0.0,  0.0, +1.0), (-mgc, +mgc))),
			Draw4DVertexTex(((+pbs, +pbs, -pbs), ( 0.0,  0.0, +1.0), (+mgc, +mgc))),

			//	z == +pbs
			Draw4DVertexTex(((-pbs, -pbs, +pbs), ( 0.0,  0.0, -1.0), (-mgc, -mgc))),
			Draw4DVertexTex(((-pbs, +pbs, +pbs), ( 0.0,  0.0, -1.0), (+mgc, -mgc))),
			Draw4DVertexTex(((+pbs, -pbs, +pbs), ( 0.0,  0.0, -1.0), (-mgc, +mgc))),
			Draw4DVertexTex(((+pbs, +pbs, +pbs), ( 0.0,  0.0, -1.0), (+mgc, +mgc)))
		]
		
		let theBoxFacetIndices: [Draw4DMeshIndexType] = [

			//	The cube has 6 square faces.
			//	Each square face has 2 triangular facets.
			//	Each triangular facet has 3 vertex indices.

			 0,  1,  2,     2,  1,  3,
			 4,  5,  6,     6,  5,  7,
			 8,  9, 10,    10,  9, 11,
			12, 13, 14,    14, 13, 15,
			16, 17, 18,    18, 17, 19,
			20, 21, 22,    22, 21, 23
		]

		let theBoxMesh = GeometryGamesMesh(
							vertices: theBoxVertices,
							indices: theBoxFacetIndices)

		let theBoxMeshBuffers = theBoxMesh.makeBuffers(device: itsDevice)

		return theBoxMeshBuffers
	}
	
	func prepareBoxInstance(
		modelData: Draw4DDocument,
		commandBuffer: MTLCommandBuffer
	) -> MTLBuffer? {
		
		if modelData.itsBoxIsEnabled {
		
			let theBoxInstanceBuffer = itsBoxInstanceBufferPool.get(forUseWith: commandBuffer)
			let theBoxInstance = theBoxInstanceBuffer.contents()
				.bindMemory(to: Draw4DInstanceDataTex.self, capacity: 1)

			theBoxInstance.pointee.itsModelViewMatrix
				= convertDouble4x4toFloat4x4(simd_double4x4(modelData.itsOrientation))

			return theBoxInstanceBuffer
			
		} else {
		
			return nil
		}
	}
	
	func makeGridTexture() -> MTLTexture? {
		
		var theTextureSize = 256

		//	Does the host support non-uniform threadgroup sizes?
		//
		//		Non-uniform threadgroup sizes are available
		//		in MTLGPUFamilyApple4 and higher, meaning
		//		an A11 GPU or newer.
		//
		let theNonuniformThreadGroupSizesAreAvailable = itsDevice.supportsFamily(.apple4)

		//	If the host doesn't support non-uniform threadgroup sizes,
		//	then to facilitate our workaround we'll increase the texture size
		//	as necessary, to ensure that it's a multiple of the thread execution width.
		//
		let theThreadExecutionWidth = itsMakeGridTextureComputePipelineState.threadExecutionWidth
		if ( !theNonuniformThreadGroupSizesAreAvailable ) {
			if (theTextureSize % theThreadExecutionWidth != 0) {
				theTextureSize = ( (theTextureSize / theThreadExecutionWidth) + 1 ) * theThreadExecutionWidth
			}
		}

		//	Create a greyscale texture using MTLPixelFormatR8Unorm.
		let theDescriptor = MTLTextureDescriptor.texture2DDescriptor(
								pixelFormat: MTLPixelFormat.r8Unorm,
								width: theTextureSize,
								height: theTextureSize,
								mipmapped: true)
		theDescriptor.usage = [MTLTextureUsage.shaderRead, MTLTextureUsage.shaderWrite]
		theDescriptor.storageMode = MTLStorageMode.private
		guard let theTexture = itsDevice.makeTexture(descriptor: theDescriptor) else {
			assertionFailure("makeGridTexture() failed to create theTexture")
			return nil
		}

		//	Set the location and width of the grid line, in pixels.
		//
		//		Note:  theGridLineLimits specify not pixels,
		//		but rather the coordinate lines between pixels.
		//		For example, to create the grid lines
		//		shown by the asterisks in this 16×16 texture
		//
		//		    * * * * * * * * * * * * * * * *
		//		    * * * * * * * * * * * * * * * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		    * *                         * *
		//		   |* *|* * * * * * * * * * * *|* *|
		//		   |* *|* * * * * * * * * * * *|* *|
		//		   0   2                       14  16
		//
		//		we'd pass theGridLineLimits = (2, 14).
		//
		let theGridLineHalfThickness = UInt16(theTextureSize / 64)	//	almost surely equals 4
		var theGridLineLimits = simd_ushort2(
									theGridLineHalfThickness,
									UInt16(theTextureSize) - theGridLineHalfThickness)

		//	Let the GPU draw grid lines into theTexture.
	 
		guard let theCommandBuffer = itsCommandQueue.makeCommandBuffer() else {
			assertionFailure("makeGridTexture() failed to create theCommandBuffer")
			return nil
		}
	 
		guard let theComputeEncoder = theCommandBuffer.makeComputeCommandEncoder() else {
			assertionFailure("makeGridTexture() failed to create theComputeEncoder")
			return nil
		}
		theComputeEncoder.label = "make wall grid mask"
		theComputeEncoder.setComputePipelineState(itsMakeGridTextureComputePipelineState)
		theComputeEncoder.setTexture(theTexture,
							index:TextureIndexCFDst)
		theComputeEncoder.setBytes(&theGridLineLimits,
							length: MemoryLayout.size(ofValue: theGridLineLimits),
							index: BufferIndexCFGridLimits)

		if (theNonuniformThreadGroupSizesAreAvailable)
		{
			//	Dispatch one thread per pixel, using the first strategy
			//	described in Apple's article
			//
			//		https://developer.apple.com/documentation/metal/calculating_threadgroup_and_grid_sizes
			//
			//	There's no reason theThreadgroupWidth must equal the threadExecutionWidth,
			//	but it's a convenient choice.
			
			//	The threadExecutionWidth is a hardware-dependent constant (typically 32).
			let theThreadgroupWidth = itsMakeGridTextureComputePipelineState.threadExecutionWidth
			
			//	The maxTotalThreadsPerThreadgroup varies according to program resource needs.
			let theThreadgroupHeight = itsMakeGridTextureComputePipelineState.maxTotalThreadsPerThreadgroup
									 / theThreadgroupWidth
			
			theComputeEncoder.dispatchThreads(
				MTLSizeMake(theTextureSize, theTextureSize, 1),
				threadsPerThreadgroup: MTLSizeMake(theThreadgroupWidth, theThreadgroupHeight, 1))
		}
		else
		{
			//	Legacy method:
			//
			//	Use the second strategy described in Apple's article cited above.
			//
			//	We've already increased theTextureSize as needed to ensure
			//	that theTextureSize is a multiple of the thread execution width.
			//	Thus by letting theThreadgroupHeight be 1, we guarantee
			//	that no threadgroup will extend beyond the bounds of the image.
			//	Therefore the compute function needn't include any "defensive code"
			//	to check for out-of-bounds pixel coordinates.
			//
			let theThreadgroupWidth = theThreadExecutionWidth
			let theThreadgroupHeight = 1

			theComputeEncoder.dispatchThreadgroups(
				MTLSizeMake(
					theTextureSize / theThreadgroupWidth,
					theTextureSize / theThreadgroupHeight,
					1),
				threadsPerThreadgroup: MTLSizeMake(theThreadgroupWidth, theThreadgroupHeight, 1))
		}

		theComputeEncoder.endEncoding()

		GenerateMipmaps(for: theTexture, commandBuffer: theCommandBuffer)

		theCommandBuffer.commit()

		//	The GPU will create theTexture's contents asynchronously,
		//	but that's OK because they're guaranteed to be ready for use
		//	in time for any jobs that we might submit later (like rendering the scene!).
		return theTexture
	}
	
	
	// MARK: -
	// MARK: guidelines

	func makeGuidelineMesh() -> GeometryGamesMeshBuffers<Draw4DVertexHSV, Draw4DMeshIndexType> {
		
		let theGuidelineRadius = 0.01
		
		//	Realize the guideline mesh as an antiprism.

		let theVertexCountPerEnd = 32

		let theSideVertices = (0 ..< theVertexCountPerEnd).flatMap() { i in
			
			let theAngle = 2.0 * Double.pi * Double(i) / Double(theVertexCountPerEnd)
			let theCosine = cos(theAngle)
			let theSine = sin(theAngle)

			return [
			
				Draw4DVertexHSV(
					pos: SIMD3<Float32>(
						Float32(theGuidelineRadius * theCosine),
						Float32(theGuidelineRadius * theSine),
						+1.0
					),
					nor: SIMD3<Float16>(
						Float16(theCosine),
						Float16(theSine),
						0.0
					),
					wgt: 0.0
				),
				
				Draw4DVertexHSV(
					pos: SIMD3<Float32>(
						Float32(theGuidelineRadius * theCosine),
						Float32(theGuidelineRadius * theSine),
						-1.0
					),
					nor: SIMD3<Float16>(
						Float16(theCosine),
						Float16(theSine),
						0.0
					),
					wgt: 1.0
				)
			]
		}
		let theBottomEndcapVertices = (0 ..< theVertexCountPerEnd).map() { i in
			
			let theAngle = 2.0 * Double.pi * Double(i) / Double(theVertexCountPerEnd)
			let theCosine = cos(theAngle)
			let theSine = sin(theAngle)

			return Draw4DVertexHSV(
				pos: SIMD3<Float32>(
					Float32(theGuidelineRadius * theCosine),
					Float32(theGuidelineRadius * theSine),
					-1.0
				),
				nor: SIMD3<Float16>(0.0, 0.0, -1.0),
				wgt: 1.0
			)
		}
		let theTopEndcapVertices = (0 ..< theVertexCountPerEnd).map() { i in
			
			let theAngle = 2.0 * Double.pi * Double(i) / Double(theVertexCountPerEnd)
			let theCosine = cos(theAngle)
			let theSine = sin(theAngle)

			return Draw4DVertexHSV(
				pos: SIMD3<Float32>(
					Float32(theGuidelineRadius * theCosine),
					Float32(theGuidelineRadius * theSine),
					+1.0
				),
				nor: SIMD3<Float16>(0.0, 0.0, +1.0),
				wgt: 0.0
			)
		}
		let theVertices = theSideVertices + theBottomEndcapVertices + theTopEndcapVertices

		//	To keep Swift's type inference system from slowing to a crawl,
		//	let's define the facet indices as Ints and then convert them
		//	to Draw4DMeshIndexType afterwards.
		//
		let theSideVertexCount = 2 * theVertexCountPerEnd
		let theSideFacets = (0 ..< theVertexCountPerEnd).flatMap() { i in

			let theIndices = [
			
				 2*i + 0,
				 2*i + 1,
				(2*i + 2) % theSideVertexCount,

				(2*i + 2) % theSideVertexCount,
				 2*i + 1,
				(2*i + 3) % theSideVertexCount
			]
			
			return theIndices
		}
		
		let beio	//	'beio' = bottom endcap index offset
			= 2 * theVertexCountPerEnd	//	side vertex count
		let theBottomEndcapFacets = (0 ... (theVertexCountPerEnd - 2)).flatMap() { i in

			let theIndices = [
			
				beio +    0   ,
				beio + (i + 2),
				beio + (i + 1)
			]
			
			return theIndices
		}
		let teio	//	'teio' = top endcap index offset
			= beio
			+ theVertexCountPerEnd	//	bottom endcap vertex count
		let theTopEndcapFacets = (0 ... (theVertexCountPerEnd - 2)).flatMap() { i in
		
			let theIndices = [
			
				teio +    0   ,
				teio + (i + 1),
				teio + (i + 2)
			]
			
			return theIndices
		}
		let theFacetsAsInts = theSideFacets + theBottomEndcapFacets + theTopEndcapFacets
		let theFacets = theFacetsAsInts.map() { i in Draw4DMeshIndexType(i) }
		
		let theGuidelineMesh = GeometryGamesMesh(
								vertices: theVertices,
								indices: theFacets)

		let theGuidelineMeshBuffers = theGuidelineMesh.makeBuffers(device: itsDevice)

		return theGuidelineMeshBuffers
	}
	
	func prepareGuidelineInstances(
		modelData: Draw4DDocument,
		commandBuffer: MTLCommandBuffer
	) -> MTLBuffer? {

		if (modelData.itsTouchMode == .movePoints
		 || modelData.itsTouchMode == .addPoints
		) {
			if let theSelectedPoint = modelData.itsSelectedPoint {
		
				let theGuidelineInstancesBuffer = itsGuidelineInstancesBufferPool.get(forUseWith: commandBuffer)
				let theGuidelineInstances = theGuidelineInstancesBuffer.contents()
					.bindMemory(to: Draw4DInstanceDataHSV.self, capacity: itsNumGuidelines)

				//	Figure out where theSelectedPoint sits,
				//	taking into account a possible animated rotation in progress.
				//	Work in box coordinates, not world coordinates.
				
				let theTransformation = composeTransformation(
					orientation: gQuaternionIdentity,	//	for box coordinates
					animatedRotation: modelData.itsAnimatedRotation)

				let (theTransformedPosition, theClampedW, _) = transformedPositionAndHue(
					position: theSelectedPoint.itsPosition,
					transformation: theTransformation)
				
				let theGreyLevels: [Float16] = [0.8750, 0.9375, 1.0000]

				let theViewMatrix = simd_double4x4(modelData.itsOrientation)

				//	Draw the x-, y- and z-lines as solid shades of grey.
				for i in 0...2 {
				
					let i1 = (i + 1)%3
					let i2 = (i + 2)%3

					//	Apple's documentation doesn't say
					//	what matrix simd_double4x4() returns,
					//	but in practice it's the zero matrix.
					var theModelMatrix = simd_double4x4()
					
					//	A permutation matrix like
					//
					//		( 0  1  0  0 )
					//		( 0  0  1  0 )
					//		( 1  0  0  0 )
					//		( 0  0  0  1 )
					//
					//	rotates the guideline from its default position along the z-axis
					//	to the x-, y- or z-axis as desired.
					//
					//	Premultiplying by
					//
					//		( 1  0    0     0 )
					//		( 0  1    0     0 )
					//		( 0  0 gBoxSize 0 )
					//		( 0  0    0     1 )
					//
					//	stretches the the guideline from length 1 to length gBoxSize
					//	before rotating it.
					//
					//	As always, "itsModelViewMatrix.columns" in the simd library's right-to-left convention
					//	is equivalent to rows in our left-to-right convention.
					//
					theModelMatrix[0][i1]	= 1.0
					theModelMatrix[1][i2]	= 1.0
					theModelMatrix[2][i]	= gBoxSize
					theModelMatrix[3][3]	= 1.0
					
					theModelMatrix[3][i1]	= theTransformedPosition[i1]
					theModelMatrix[3][i2]	= theTransformedPosition[i2]
					
					let theModelViewMatrix = theViewMatrix * theModelMatrix

					theGuidelineInstances[i].itsModelViewMatrix
						= convertDouble4x4toFloat4x4(theModelViewMatrix)

					theGuidelineInstances[i].itsHSV0[0] = 0.0
					theGuidelineInstances[i].itsHSV0[1] = 0.0
					theGuidelineInstances[i].itsHSV0[2] = theGreyLevels[i]

					theGuidelineInstances[i].itsHSV1[0] = 0.0
					theGuidelineInstances[i].itsHSV1[1] = 0.0
					theGuidelineInstances[i].itsHSV1[2] = theGreyLevels[i]

					theGuidelineInstances[i].itsOpacity = 1.0
				}

				//	Draw the w-line as a spectrum.

				//	In theModelMatrix, the upper-left 3×3 block
				//	can be any rotation that takes the z direction (0,0,1)
				//	to the "faux w direction" (√⅓,√⅓,√⅓).
				let theOffset = theClampedW * sqrt(1.0/3.0)
				let theModelMatrix = matrix_double4x4(columns:(
					SIMD4<Double>( sqrt(  0.5  ), -sqrt(  0.5  ),        0.0,     0.0 ),
					SIMD4<Double>( sqrt(1.0/6.0),  sqrt(1.0/6.0), -sqrt(2.0/3.0), 0.0 ),
					SIMD4<Double>( sqrt(1.0/3.0),  sqrt(1.0/3.0),  sqrt(1.0/3.0), 0.0 ),
					SIMD4<Double>(
						theTransformedPosition[0] - theOffset,
						theTransformedPosition[1] - theOffset,
						theTransformedPosition[2] - theOffset,
						1.0
					)
				))
				let theModelViewMatrix = theViewMatrix * theModelMatrix
				
				theGuidelineInstances[3].itsModelViewMatrix
					= convertDouble4x4toFloat4x4(theModelViewMatrix)

				theGuidelineInstances[3].itsHSV0[0] = Float16(gHueKata)
				theGuidelineInstances[3].itsHSV0[1] = 1.0
				theGuidelineInstances[3].itsHSV0[2] = 1.0

				theGuidelineInstances[3].itsHSV1[0] = Float16(gHueAna)
				theGuidelineInstances[3].itsHSV1[1] = 1.0
				theGuidelineInstances[3].itsHSV1[2] = 1.0

				theGuidelineInstances[3].itsOpacity = 1.0

				//	Success
				return theGuidelineInstancesBuffer
				
			} else {
			
				return nil
			}
			
		} else {
		
			return nil
		}
	}

	
	// MARK: -
	// MARK: guideplanes

	func makeGuideplaneMesh() -> GeometryGamesMeshBuffers<Draw4DVertexRGB, Draw4DMeshIndexType> {
		
		let theGuideplaneVertices: [Draw4DVertexRGB] = [

			//	first face
			Draw4DVertexRGB((( 0.0, -1.0, -1.0), (-1.0, 0.0,  0.0))),
			Draw4DVertexRGB((( 0.0, -1.0, +1.0), (-1.0, 0.0,  0.0))),
			Draw4DVertexRGB((( 0.0, +1.0, -1.0), (-1.0, 0.0,  0.0))),
			Draw4DVertexRGB((( 0.0, +1.0, +1.0), (-1.0, 0.0,  0.0))),

			//	second face,
			//	with opposite normal vector
			Draw4DVertexRGB((( 0.0, -1.0, -1.0), (+1.0, 0.0,  0.0))),
			Draw4DVertexRGB((( 0.0, -1.0, +1.0), (+1.0, 0.0,  0.0))),
			Draw4DVertexRGB((( 0.0, +1.0, -1.0), (+1.0, 0.0,  0.0))),
			Draw4DVertexRGB((( 0.0, +1.0, +1.0), (+1.0, 0.0,  0.0)))
		]
		
		let theGuideplaneIndices: [Draw4DMeshIndexType] = [

			0, 1, 2,   2, 1, 3,
			4, 6, 5,   5, 6, 7	//	opposite winding order
		]

		let theGuideplaneMesh = GeometryGamesMesh(
									vertices: theGuideplaneVertices,
									indices: theGuideplaneIndices)

		let theGuideplaneMeshBuffers = theGuideplaneMesh.makeBuffers(device: itsDevice)

		return theGuideplaneMeshBuffers
	}
	
	func prepareGuideplaneInstances(
		modelData: Draw4DDocument,
		commandBuffer: MTLCommandBuffer
	) -> MTLBuffer? {

		if (modelData.itsTouchMode == .movePoints
		 || modelData.itsTouchMode == .addPoints
		) {
			if let theSelectedPoint = modelData.itsSelectedPoint {
		
				let theGuideplaneInstancesBuffer = itsGuideplaneInstancesBufferPool.get(forUseWith: commandBuffer)
				let theGuideplaneInstances = theGuideplaneInstancesBuffer.contents()
					.bindMemory(to: Draw4DInstanceDataRGB.self, capacity: itsNumGuideplanes)

				//	Figure out where the eye sits model coordinates,
				//	so we can draw the guideplane rectangles on the far side
				//	of theSelectedPoint, as perceived by the user.
				
				let theEyeInModelCoordinates
					= modelData.itsOrientation.inverse.act(gEyePositionInWorldCoordinates)

				//	Figure out where theSelectedPoint sits,
				//	taking into account a possible animated rotation in progress.
				//	Work in box coordinates, not world coordinates.
				
				let theTransformation = composeTransformation(
					orientation: gQuaternionIdentity,	//	for box coordinates
					animatedRotation: modelData.itsAnimatedRotation)

				var (theTransformedPosition, _, theHue) = transformedPositionAndHue(
					position: theSelectedPoint.itsPosition,
					transformation: theTransformation)
					
				//	Aesthetic kludge
				if abs(theHue - 0.1875) < 0.01 {	//	sickly greenish yellow
					theHue = 1.0 / 6.0				//	pure cheerful yellow
				}
				
				let theRGBColor = HSVtoRGB(hue: theHue, saturation: 1.0, value: 1.0)

				//	Set a constant opacity.
				let theGuideplaneOpacity: Float16 = 0.75
					
				//	Figure out where to place the three guideplane rectangles.

				var theBorder = SIMD3<Double>.zero
				var theHalfWidth = SIMD3<Double>.zero
				var theCenter = SIMD3<Double>.zero
				for i in 0...2 {
					if theEyeInModelCoordinates[i] <= theTransformedPosition[i] {
						theBorder[i] = +gBoxSize
						theHalfWidth[i] = 0.5 * (gBoxSize - theTransformedPosition[i])
						theCenter[i] = theBorder[i] - theHalfWidth[i]
					} else {	//	theEyeInModelCoordinates[i] > theTransformedPosition[i]
						theBorder[i] = -gBoxSize
						theHalfWidth[i] = 0.5 * (theTransformedPosition[i] - (-gBoxSize))
						theCenter[i] = theBorder[i] + theHalfWidth[i]
					}
				}
				for i0 in 0...2 {
				
					let i1 = (i0 + 1)%3
					let i2 = (i0 + 2)%3
				
					//	Apple's documentation doesn't say
					//	what matrix simd_double4x4() returns,
					//	but in practice it's the zero matrix.
					var theModelMatrix = simd_double4x4()
					
					theModelMatrix[0][i0] = 1.0
					theModelMatrix[1][i1] = theHalfWidth[i1]
					theModelMatrix[2][i2] = theHalfWidth[i2]
					
					theModelMatrix[3][i0] = theTransformedPosition[i0]
					theModelMatrix[3][i1] = theCenter[i1]
					theModelMatrix[3][i2] = theCenter[i2]
					theModelMatrix[3][3]  = 1.0
					
					let theViewMatrix = simd_double4x4(modelData.itsOrientation)
					
					let theModelViewMatrix = theViewMatrix * theModelMatrix

					theGuideplaneInstances[i0].itsModelViewMatrix
						= convertDouble4x4toFloat4x4(theModelViewMatrix)
					theGuideplaneInstances[i0].itsRGB = theRGBColor
					theGuideplaneInstances[i0].itsOpacity = theGuideplaneOpacity
				}

				return theGuideplaneInstancesBuffer
				
			} else {
			
				return nil
			}
			
		} else {
		
			return nil
		}
	}
}


// MARK: -
// MARK: Characteristic size

//	At render time the characteristic size will be used to deduce
//	the octahedron view's width and height in intrinsic units.

func characteristicViewSize(
	width: Double,	//	typically in pixels or points
	height: Double	//	typically in pixels or points
) -> Double {

	//	This is the *only* place that specifies the dependence
	//	of the characteristic size on the view dimensions.
	//	If you want to change the definition, for example
	//	from min(width,height) to sqrt(width*height),
	//	this is the only place you need to do it.
	
	return min(width, height)
}

//	The characteristic size will always correspond
//	to the number of intrinsic units (IU) given below,
//	even as the user resizes the view and thus changes
//	the number of pixels lying within it.
//
let gCharacteristicSizeIU = 2.0 * gBoxSize	//	view's inradius is slightly greater than 1

func intrinsicUnitsPerPixelOrPoint(
	viewWidth: Double,	//	typically in pixels or points
	viewHeight: Double	//	typically in pixels or points
) -> Double {	//	number of intrinsic units per pixel or point

	precondition(
		viewWidth > 0.0 && viewHeight > 0.0,
		"intrinsicUnitsPerPixelOrPoint() received non-positive viewWidth or viewHeight")

	let theCharacteristicSizePp = characteristicViewSize(
									width: viewWidth,
									height: viewHeight)

	let theIntrinsicUnitsPerPixelOrPoint = gCharacteristicSizeIU / theCharacteristicSizePp
	
	return theIntrinsicUnitsPerPixelOrPoint
}


// MARK: -
// MARK: Transformations

func composeTransformation(
	orientation: simd_quatd,
	animatedRotation: AnimatedRotation?
) -> QuaternionPair {

	let theOrientationPart = QuaternionPair(
		left: orientation,
		right: orientation.inverse)

	let theTransformation: QuaternionPair
	if let theAnimatedRotation = animatedRotation {	//	animation in progress?

		let theElapsedTime = CFAbsoluteTimeGetCurrent() - theAnimatedRotation.itsStartTime
		let theTimeFraction = theElapsedTime / theAnimatedRotation.itsTotalDuration
		let θ = 0.5 * theAnimatedRotation.itsTotalAngle * theTimeFraction
		
		let theAnimatedPart = QuaternionPair(
			left: simd_quatd(
					real: cos(θ),
					imag: sin(θ) * theAnimatedRotation.itsLeftImaginaryUnitVector),
			right: simd_quatd(
					real: cos(θ),
					imag: sin(θ) * theAnimatedRotation.itsRightImaginaryUnitVector))
	
		//	Note that we perform theAnimatedPart before theOrientationPart.
		theTransformation = QuaternionPair(
			left:  theOrientationPart.left * theAnimatedPart.left,
			right: theAnimatedPart.right * theOrientationPart.right)
			
	} else {	//	no animation in progress
	
		theTransformation = theOrientationPart
	}
	
	return theTransformation
}

func transformedPositionAndHue(
	position: simd_quatd,
	transformation: QuaternionPair	//	for when an animation rotation is in progress
) -> (SIMD3<Double>, Double, Double) {	//	(transformed and sheared position, clamped w, hue)

	let theTransformedPositionAsQuaternion
		= transformation.left * position * transformation.right
		
	let theOffset = theTransformedPositionAsQuaternion.real * g4DShearFactor
	let theTransformedPosition = theTransformedPositionAsQuaternion.imag
							   + SIMD3<Double>(theOffset, theOffset, theOffset)

	//	Assign the hues from red (gHueKata) to purple (gHueAna)
	//	to w-coordinates in the range [-1.0, +1.0],
	//	clamping w-coordinates outside that range.
	//	This lets us produce colorful figures while still
	//	allowing a margin between the figure and the box walls
	//	at x, y, z = ±gBoxSize.  (If we didn't care about the margin,
	//	we could instead map those hues to [-gBoxSize, +gBoxSize].)
	let theClampedW = max(-1.0, min(+1.0,
		theTransformedPositionAsQuaternion.real))	//	∈ [-1.0, +1.0]
	let t =  0.5  +  0.5 * theClampedW	//	∈ [0.0, 1.0]
	let theHue
		= (1.0 - t) * gHueKata
		+     t     * gHueAna
	
	return (theTransformedPosition, theClampedW, theHue)
}
