openusd 1.0.0
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.
- checksums.yaml +7 -0
- data/.rubocop.yml +47 -0
- data/.yardopts +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +163 -0
- data/Rakefile +91 -0
- data/benchmark/RESULTS.md +16 -0
- data/benchmark/parser.rb +53 -0
- data/docs/CLI_SMOKE_TEST.md +29 -0
- data/exe/openusd +6 -0
- data/lib/openusd/asset_resolver.rb +70 -0
- data/lib/openusd/attribute.rb +103 -0
- data/lib/openusd/attribute_spec.rb +120 -0
- data/lib/openusd/cli.rb +115 -0
- data/lib/openusd/composition.rb +190 -0
- data/lib/openusd/errors.rb +41 -0
- data/lib/openusd/format/registry.rb +52 -0
- data/lib/openusd/format/usda/lexer.rb +258 -0
- data/lib/openusd/format/usda/parser.rb +338 -0
- data/lib/openusd/format/usda/property_merger.rb +47 -0
- data/lib/openusd/format/usda/reference_metadata.rb +37 -0
- data/lib/openusd/format/usda/writer.rb +296 -0
- data/lib/openusd/format/usdz/reader.rb +195 -0
- data/lib/openusd/format/usdz/writer.rb +145 -0
- data/lib/openusd/layer.rb +126 -0
- data/lib/openusd/metadata_view.rb +26 -0
- data/lib/openusd/path.rb +158 -0
- data/lib/openusd/prim.rb +151 -0
- data/lib/openusd/prim_spec.rb +179 -0
- data/lib/openusd/relationship.rb +56 -0
- data/lib/openusd/relationship_spec.rb +57 -0
- data/lib/openusd/schema/base.rb +61 -0
- data/lib/openusd/schema/camera.rb +30 -0
- data/lib/openusd/schema/material.rb +17 -0
- data/lib/openusd/schema/mesh.rb +31 -0
- data/lib/openusd/schema/scope.rb +10 -0
- data/lib/openusd/schema/xform.rb +49 -0
- data/lib/openusd/stage.rb +262 -0
- data/lib/openusd/types.rb +168 -0
- data/lib/openusd/value.rb +100 -0
- data/lib/openusd/version.rb +6 -0
- data/lib/openusd.rb +40 -0
- metadata +85 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
# An authored relationship in a Layer.
|
|
5
|
+
class RelationshipSpec
|
|
6
|
+
# Sentinel distinguishing no target opinion from an authored empty list.
|
|
7
|
+
UNAUTHORED = Object.new.freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :targets, :metadata
|
|
10
|
+
attr_accessor :custom
|
|
11
|
+
|
|
12
|
+
def initialize(name, targets: UNAUTHORED, custom: false, metadata: {})
|
|
13
|
+
@name = validate_name(name)
|
|
14
|
+
@custom = custom == true
|
|
15
|
+
@metadata = metadata.dup
|
|
16
|
+
@targets = []
|
|
17
|
+
@targets_authored = false
|
|
18
|
+
self.targets = targets unless targets.equal?(UNAUTHORED)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def targets=(paths)
|
|
22
|
+
@targets = Array(paths).map { |path| Path.parse(path) }
|
|
23
|
+
@targets_authored = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Whether targets, including an empty list, are authored.
|
|
27
|
+
def targets_authored?
|
|
28
|
+
@targets_authored
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Add a target unless it is already present.
|
|
32
|
+
# @return [Path]
|
|
33
|
+
def add_target(path)
|
|
34
|
+
target = Path.parse(path)
|
|
35
|
+
@targets << target unless @targets.include?(target)
|
|
36
|
+
@targets_authored = true
|
|
37
|
+
target
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Hash] semantic representation used for equality
|
|
41
|
+
def to_h
|
|
42
|
+
{
|
|
43
|
+
name: name, targets: targets, targets_authored: targets_authored?,
|
|
44
|
+
custom: custom, metadata: metadata
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def validate_name(value)
|
|
51
|
+
name = String(value)
|
|
52
|
+
return name.dup.freeze if Path::PROPERTY_NAME.match?(name)
|
|
53
|
+
|
|
54
|
+
raise PathError, "invalid relationship name: #{name.inspect}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
# Typed convenience wrappers for commonly authored prim schemas.
|
|
5
|
+
module Schema
|
|
6
|
+
# Base for typed convenience wrappers around Stage prims.
|
|
7
|
+
class Base
|
|
8
|
+
class << self
|
|
9
|
+
attr_reader :type_name
|
|
10
|
+
|
|
11
|
+
# Declare the prim type represented by a wrapper.
|
|
12
|
+
# @api private
|
|
13
|
+
def schema_type(name)
|
|
14
|
+
@type_name = name.freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Define and wrap a prim.
|
|
18
|
+
# @return [Base]
|
|
19
|
+
def define(stage, path)
|
|
20
|
+
new(stage.define_prim(path, type_name))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Wrap a prim only when its type matches.
|
|
24
|
+
# @return [Base, nil]
|
|
25
|
+
def get(stage, path)
|
|
26
|
+
prim = stage.prim_at(path)
|
|
27
|
+
return unless prim&.type_name == type_name
|
|
28
|
+
|
|
29
|
+
new(prim)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :prim
|
|
34
|
+
|
|
35
|
+
def initialize(prim)
|
|
36
|
+
@prim = prim
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Stage] owning stage
|
|
40
|
+
def stage
|
|
41
|
+
prim.stage
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Path] wrapped prim path
|
|
45
|
+
def path
|
|
46
|
+
prim.path
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def get(name)
|
|
52
|
+
prim.attribute(name)&.get
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def set(name, type_name, value)
|
|
56
|
+
prim.create_attribute(name, type_name).set(value)
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
module Schema
|
|
5
|
+
# Convenience API for common Camera attributes.
|
|
6
|
+
class Camera < Base
|
|
7
|
+
schema_type "Camera"
|
|
8
|
+
|
|
9
|
+
# @return [Float, nil] focal length in tenths of a scene unit
|
|
10
|
+
def focal_length
|
|
11
|
+
get("focalLength")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Author the focal length.
|
|
15
|
+
def focal_length=(value)
|
|
16
|
+
set("focalLength", "float", value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Array<Float>, nil] near and far clipping distances
|
|
20
|
+
def clipping_range
|
|
21
|
+
get("clippingRange")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Author near and far clipping distances.
|
|
25
|
+
def clipping_range=(value)
|
|
26
|
+
set("clippingRange", "float2", value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
module Schema
|
|
5
|
+
# Convenience API for Material prims.
|
|
6
|
+
class Material < Base
|
|
7
|
+
schema_type "Material"
|
|
8
|
+
|
|
9
|
+
# Bind this material to another prim.
|
|
10
|
+
# @return [Material]
|
|
11
|
+
def bind(geometry_prim)
|
|
12
|
+
geometry_prim.create_relationship("material:binding").set_targets([path])
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
module Schema
|
|
5
|
+
# Convenience API for core Mesh topology and point attributes.
|
|
6
|
+
class Mesh < Base
|
|
7
|
+
schema_type "Mesh"
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
points: ["points", "point3f[]"],
|
|
11
|
+
normals: ["normals", "normal3f[]"],
|
|
12
|
+
face_vertex_counts: ["faceVertexCounts", "int[]"],
|
|
13
|
+
face_vertex_indices: ["faceVertexIndices", "int[]"],
|
|
14
|
+
extent: ["extent", "float3[]"]
|
|
15
|
+
}.each do |method_name, (attribute_name, type_name)|
|
|
16
|
+
define_method(method_name) { get(attribute_name) }
|
|
17
|
+
define_method("#{method_name}=") { |value| set(attribute_name, type_name, value) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Token, nil] authored subdivision scheme
|
|
21
|
+
def subdivision_scheme
|
|
22
|
+
get("subdivisionScheme")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Author the subdivision scheme token.
|
|
26
|
+
def subdivision_scheme=(value)
|
|
27
|
+
set("subdivisionScheme", "token", value)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
module Schema
|
|
5
|
+
# Convenience API for Xform prims and common transform operations.
|
|
6
|
+
class Xform < Base
|
|
7
|
+
schema_type "Xform"
|
|
8
|
+
|
|
9
|
+
# @return [Array<Float>, nil] translation operation
|
|
10
|
+
def translate
|
|
11
|
+
get("xformOp:translate")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Author the translation operation.
|
|
15
|
+
def translate=(value)
|
|
16
|
+
set_operation("xformOp:translate", "double3", value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Array<Float>, nil] XYZ rotation operation
|
|
20
|
+
def rotate_xyz
|
|
21
|
+
get("xformOp:rotateXYZ")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Author the XYZ rotation operation.
|
|
25
|
+
def rotate_xyz=(value)
|
|
26
|
+
set_operation("xformOp:rotateXYZ", "float3", value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Array<Float>, nil] scale operation
|
|
30
|
+
def scale
|
|
31
|
+
get("xformOp:scale")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Author the scale operation.
|
|
35
|
+
def scale=(value)
|
|
36
|
+
set_operation("xformOp:scale", "float3", value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def set_operation(name, type_name, value)
|
|
42
|
+
set(name, type_name, value)
|
|
43
|
+
order = prim.attribute("xformOpOrder")&.get || []
|
|
44
|
+
prim.create_attribute("xformOpOrder", "token[]").set(order + [name]) unless order.include?(name)
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
# Composed, editable view of one root layer and its composition arcs.
|
|
5
|
+
class Stage
|
|
6
|
+
attr_reader :root_layer, :resolver, :pseudo_root, :edit_target
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
# Open a composed stage.
|
|
10
|
+
# @return [Stage]
|
|
11
|
+
def open(path, missing_assets: :error)
|
|
12
|
+
expanded = File.expand_path(path)
|
|
13
|
+
resolver = AssetResolver.new(missing_assets: missing_assets)
|
|
14
|
+
new(Layer.open(expanded), resolver: resolver)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Create an empty file-backed stage.
|
|
18
|
+
# @return [Stage]
|
|
19
|
+
def create(identifier)
|
|
20
|
+
new(Layer.create(identifier))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Create an anonymous in-memory stage.
|
|
24
|
+
# @return [Stage]
|
|
25
|
+
def create_in_memory
|
|
26
|
+
new(Layer.create("anonymous:#{object_id}:#{Process.clock_gettime(Process::CLOCK_MONOTONIC)}"))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(root_layer, resolver: AssetResolver.new)
|
|
31
|
+
@root_layer = root_layer
|
|
32
|
+
@resolver = resolver
|
|
33
|
+
@edit_target = root_layer
|
|
34
|
+
@pseudo_root = PseudoRoot.new(self)
|
|
35
|
+
@composition = nil
|
|
36
|
+
@index = nil
|
|
37
|
+
@layer_cache = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def edit_target=(layer)
|
|
41
|
+
raise OpenUSD::TypeError, "edit target must be a Layer" unless layer.is_a?(Layer)
|
|
42
|
+
raise CompositionError, "edit target is not in this stage's layer stack" unless layer_stack.include?(layer)
|
|
43
|
+
|
|
44
|
+
@edit_target = layer
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Find an active composed prim.
|
|
48
|
+
# @return [Prim, PseudoRoot, nil]
|
|
49
|
+
def prim_at(path)
|
|
50
|
+
parsed = Path.parse(path)
|
|
51
|
+
return pseudo_root if parsed.to_s == "/"
|
|
52
|
+
return nil if parsed.property?
|
|
53
|
+
return nil unless composed_index.key?(parsed.to_s)
|
|
54
|
+
|
|
55
|
+
prim = Prim.new(self, parsed)
|
|
56
|
+
prim.active? ? prim : nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Prim, nil] composed default prim
|
|
60
|
+
def default_prim
|
|
61
|
+
name = root_layer.metadata["defaultPrim"]
|
|
62
|
+
prim_at("/#{name}") if name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Define a prim and any missing ancestors in the edit target.
|
|
66
|
+
# @return [Prim]
|
|
67
|
+
def define_prim(path, type_name = nil)
|
|
68
|
+
parsed = validate_prim_path(path)
|
|
69
|
+
spec = ensure_spec(edit_target, parsed)
|
|
70
|
+
spec.specifier = :def
|
|
71
|
+
spec.type_name = type_name.to_s if type_name
|
|
72
|
+
invalidate!
|
|
73
|
+
Prim.new(self, parsed)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Remove or deactivate a prim in the edit target.
|
|
77
|
+
# @return [Stage]
|
|
78
|
+
def remove_prim(path)
|
|
79
|
+
parsed = validate_prim_path(path)
|
|
80
|
+
remove_spec(edit_target, parsed)
|
|
81
|
+
invalidate!
|
|
82
|
+
if composed_index.key?(parsed.to_s)
|
|
83
|
+
blocker = ensure_spec(edit_target, parsed)
|
|
84
|
+
blocker.specifier = :over
|
|
85
|
+
blocker.metadata["active"] = false
|
|
86
|
+
end
|
|
87
|
+
invalidate!
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Traverse active prims depth-first.
|
|
92
|
+
# @return [Enumerator, Stage]
|
|
93
|
+
def traverse
|
|
94
|
+
return enum_for(__method__) unless block_given?
|
|
95
|
+
|
|
96
|
+
walk = lambda do |prim|
|
|
97
|
+
yield prim
|
|
98
|
+
prim.children.each { |child| walk.call(child) }
|
|
99
|
+
end
|
|
100
|
+
pseudo_root.children.each { |prim| walk.call(prim) }
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Save the root layer to its identifier.
|
|
105
|
+
# @return [Stage]
|
|
106
|
+
def save
|
|
107
|
+
root_layer.save
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Export the root layer by destination extension.
|
|
112
|
+
# @return [Stage]
|
|
113
|
+
def export(path)
|
|
114
|
+
root_layer.export(path)
|
|
115
|
+
self
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Author a variant choice for a prim.
|
|
119
|
+
# @return [Stage]
|
|
120
|
+
def set_variant_selection(path, set_name, choice)
|
|
121
|
+
prim = prim_at(path)
|
|
122
|
+
raise CompositionError, "prim not found: #{path}" unless prim
|
|
123
|
+
|
|
124
|
+
choices = prim.variant_sets[set_name.to_s]
|
|
125
|
+
raise CompositionError, "variant set not found: #{set_name}" unless choices
|
|
126
|
+
raise CompositionError, "variant not found: #{choice}" unless choices.key?(choice.to_s)
|
|
127
|
+
|
|
128
|
+
spec = ensure_spec(edit_target, Path.parse(path))
|
|
129
|
+
spec.metadata["variants"] ||= {}
|
|
130
|
+
spec.metadata["variants"][set_name.to_s] = choice.to_s
|
|
131
|
+
invalidate!
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @api private
|
|
136
|
+
# @return [Array<PrimSpec>] strongest-to-weakest opinions
|
|
137
|
+
def opinions_for(path)
|
|
138
|
+
composed_index.fetch(Path.parse(path).to_s, [])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @api private
|
|
142
|
+
# @return [Array<Path>] direct composed child paths
|
|
143
|
+
def child_paths(path)
|
|
144
|
+
prefix = "#{Path.parse(path)}/"
|
|
145
|
+
paths = composed_index.keys.select do |candidate|
|
|
146
|
+
candidate.start_with?(prefix) && !candidate.delete_prefix(prefix).include?("/")
|
|
147
|
+
end
|
|
148
|
+
paths.sort.map { |candidate| Path.parse(candidate) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @api private
|
|
152
|
+
# @return [Array<Path>] composed root paths
|
|
153
|
+
def root_paths
|
|
154
|
+
composed_index.keys.select { |path| path.count("/") == 1 }.sort.map { |path| Path.parse(path) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Find or author an attribute spec in the edit target.
|
|
158
|
+
# @api private
|
|
159
|
+
def author_attribute(path, name, type_name)
|
|
160
|
+
spec = ensure_spec(edit_target, Path.parse(path))
|
|
161
|
+
property = spec.property_named(name)
|
|
162
|
+
validate_property_kind!(property, AttributeSpec, "#{name} is authored as a relationship")
|
|
163
|
+
property ||= spec.add_property(AttributeSpec.new(name, type_name || "token"))
|
|
164
|
+
property.type_name = type_name if type_name
|
|
165
|
+
invalidate!
|
|
166
|
+
property
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Find or author a relationship spec in the edit target.
|
|
170
|
+
# @api private
|
|
171
|
+
def author_relationship(path, name)
|
|
172
|
+
spec = ensure_spec(edit_target, Path.parse(path))
|
|
173
|
+
property = spec.property_named(name)
|
|
174
|
+
validate_property_kind!(property, RelationshipSpec, "#{name} is authored as an attribute")
|
|
175
|
+
property ||= spec.add_property(RelationshipSpec.new(name))
|
|
176
|
+
invalidate!
|
|
177
|
+
property
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Author a prim type in the edit target.
|
|
181
|
+
# @api private
|
|
182
|
+
def set_prim_type(path, type_name)
|
|
183
|
+
spec = ensure_spec(edit_target, Path.parse(path))
|
|
184
|
+
spec.type_name = type_name&.to_s
|
|
185
|
+
invalidate!
|
|
186
|
+
type_name
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Apply one metadata mutation to an edit-target prim spec.
|
|
190
|
+
# @api private
|
|
191
|
+
def author_prim_metadata(path, operation, key, value)
|
|
192
|
+
metadata = ensure_spec(edit_target, Path.parse(path)).metadata
|
|
193
|
+
operation == :set ? metadata[key] = value : metadata.delete(key)
|
|
194
|
+
invalidate!
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# @return [Array<Layer>] layers participating in composition
|
|
198
|
+
def layer_stack
|
|
199
|
+
composed_index
|
|
200
|
+
@composition.layers
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @api private
|
|
204
|
+
# Invalidate the cached composition index after an edit.
|
|
205
|
+
def invalidate!
|
|
206
|
+
@index = nil
|
|
207
|
+
@composition = nil
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def composed_index
|
|
214
|
+
return @index if @index
|
|
215
|
+
|
|
216
|
+
@composition = Composition.new(root_layer, resolver: resolver, layer_cache: @layer_cache)
|
|
217
|
+
@index = @composition.build
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def validate_prim_path(path)
|
|
221
|
+
parsed = Path.parse(path)
|
|
222
|
+
invalid = !parsed.absolute? || parsed.property? || parsed.to_s == "/"
|
|
223
|
+
raise PathError, "absolute non-root prim path required" if invalid
|
|
224
|
+
|
|
225
|
+
parsed
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def ensure_spec(layer, path)
|
|
229
|
+
existing = layer.prim_at(path)
|
|
230
|
+
return existing if existing
|
|
231
|
+
|
|
232
|
+
names = path.to_s.delete_prefix("/").split("/")
|
|
233
|
+
parent = nil
|
|
234
|
+
names.each do |name|
|
|
235
|
+
child = parent ? parent.child_named(name) : layer.root_prim_named(name)
|
|
236
|
+
unless child
|
|
237
|
+
child = PrimSpec.new(name, specifier: :over)
|
|
238
|
+
parent ? parent.add_child(child) : layer.add_root_prim(child)
|
|
239
|
+
end
|
|
240
|
+
parent = child
|
|
241
|
+
end
|
|
242
|
+
parent
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def remove_spec(layer, path)
|
|
246
|
+
spec = layer.prim_at(path)
|
|
247
|
+
return unless spec
|
|
248
|
+
|
|
249
|
+
if spec.parent
|
|
250
|
+
spec.parent.remove_child(spec.name)
|
|
251
|
+
else
|
|
252
|
+
layer.remove_root_prim(spec.name)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def validate_property_kind!(property, expected_class, message)
|
|
257
|
+
return if property.nil? || property.is_a?(expected_class)
|
|
258
|
+
|
|
259
|
+
raise OpenUSD::TypeError, message
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
# Registry and validation for built-in USD value types.
|
|
5
|
+
module Types
|
|
6
|
+
# Integer ranges enforced when authoring.
|
|
7
|
+
INTEGER_RANGES = {
|
|
8
|
+
"int" => (-(2**31)...(2**31)),
|
|
9
|
+
"uint" => (0...(2**32)),
|
|
10
|
+
"int64" => (-(2**63)...(2**63)),
|
|
11
|
+
"uint64" => (0...(2**64))
|
|
12
|
+
}.freeze
|
|
13
|
+
# Floating-point scalar type names.
|
|
14
|
+
FLOAT_TYPES = %w[half float double].freeze
|
|
15
|
+
# Vector and quaternion type names mapped to component counts.
|
|
16
|
+
VECTOR_TYPES = {
|
|
17
|
+
"float2" => 2, "float3" => 3, "float4" => 4,
|
|
18
|
+
"double2" => 2, "double3" => 3, "double4" => 4,
|
|
19
|
+
"half2" => 2, "half3" => 3, "half4" => 4,
|
|
20
|
+
"int2" => 2, "int3" => 3, "int4" => 4,
|
|
21
|
+
"point3f" => 3, "normal3f" => 3, "color3f" => 3,
|
|
22
|
+
"color4f" => 4, "texCoord2f" => 2, "texCoord3f" => 3,
|
|
23
|
+
"quatf" => 4, "quatd" => 4, "quath" => 4
|
|
24
|
+
}.freeze
|
|
25
|
+
# Matrix type names mapped to dimensions.
|
|
26
|
+
MATRIX_TYPES = { "matrix2d" => 2, "matrix3d" => 3, "matrix4d" => 4 }.freeze
|
|
27
|
+
# Built-in scalar type names.
|
|
28
|
+
SCALAR_TYPES = %w[bool int uint int64 uint64 half float double string token asset].freeze
|
|
29
|
+
# Internal dispatch table for scalar normalization.
|
|
30
|
+
SIMPLE_COERCERS = {
|
|
31
|
+
"bool" => :coerce_bool,
|
|
32
|
+
"half" => :coerce_float,
|
|
33
|
+
"float" => :coerce_float,
|
|
34
|
+
"double" => :coerce_float,
|
|
35
|
+
"string" => :coerce_string,
|
|
36
|
+
"token" => :coerce_token,
|
|
37
|
+
"asset" => :coerce_asset
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# Whether the registry recognizes a type name.
|
|
43
|
+
def known?(type_name)
|
|
44
|
+
name = base_type(type_name)
|
|
45
|
+
SCALAR_TYPES.include?(name) || VECTOR_TYPES.key?(name) || MATRIX_TYPES.key?(name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether the type is an array type.
|
|
49
|
+
def array?(type_name)
|
|
50
|
+
String(type_name).end_with?("[]")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Remove the array suffix from a type name.
|
|
54
|
+
def base_type(type_name)
|
|
55
|
+
String(type_name).delete_suffix("[]")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validate and normalize a Ruby value for a USD type.
|
|
59
|
+
# Unknown types are intentionally preserved for forward compatibility.
|
|
60
|
+
def coerce(type_name, value)
|
|
61
|
+
name = String(type_name)
|
|
62
|
+
return coerce_array(base_type(name), value) if array?(name)
|
|
63
|
+
return value unless known?(name)
|
|
64
|
+
|
|
65
|
+
coerce_scalar(name, value)
|
|
66
|
+
rescue OpenUSD::TypeError
|
|
67
|
+
raise
|
|
68
|
+
rescue StandardError
|
|
69
|
+
invalid!(name, value)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Validate a Ruby value and return its normalized representation.
|
|
73
|
+
def validate!(type_name, value)
|
|
74
|
+
coerce(type_name, value)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def coerce_array(type_name, value)
|
|
78
|
+
invalid!("#{type_name}[]", value) unless value.is_a?(Array)
|
|
79
|
+
|
|
80
|
+
value.map { |element| coerce_scalar(type_name, element) }.freeze
|
|
81
|
+
end
|
|
82
|
+
private_class_method :coerce_array
|
|
83
|
+
|
|
84
|
+
def coerce_scalar(type_name, value)
|
|
85
|
+
coercer = SIMPLE_COERCERS[type_name]
|
|
86
|
+
return send(coercer, value) if coercer
|
|
87
|
+
return coerce_integer(type_name, value) if INTEGER_RANGES.key?(type_name)
|
|
88
|
+
return coerce_vector(type_name, value) if VECTOR_TYPES.key?(type_name)
|
|
89
|
+
return coerce_matrix(type_name, value) if MATRIX_TYPES.key?(type_name)
|
|
90
|
+
return value unless known?(type_name)
|
|
91
|
+
|
|
92
|
+
invalid!(type_name, value)
|
|
93
|
+
end
|
|
94
|
+
private_class_method :coerce_scalar
|
|
95
|
+
|
|
96
|
+
def coerce_bool(value)
|
|
97
|
+
return value if [true, false].include?(value)
|
|
98
|
+
|
|
99
|
+
invalid!("bool", value)
|
|
100
|
+
end
|
|
101
|
+
private_class_method :coerce_bool
|
|
102
|
+
|
|
103
|
+
def coerce_integer(type_name, value)
|
|
104
|
+
range = INTEGER_RANGES.fetch(type_name)
|
|
105
|
+
return value if value.is_a?(Integer) && range.cover?(value)
|
|
106
|
+
|
|
107
|
+
invalid!(type_name, value)
|
|
108
|
+
end
|
|
109
|
+
private_class_method :coerce_integer
|
|
110
|
+
|
|
111
|
+
def coerce_float(value)
|
|
112
|
+
return value.to_f if value.is_a?(Numeric)
|
|
113
|
+
|
|
114
|
+
invalid!("floating-point", value)
|
|
115
|
+
end
|
|
116
|
+
private_class_method :coerce_float
|
|
117
|
+
|
|
118
|
+
def coerce_string(value)
|
|
119
|
+
return value.dup.freeze if value.instance_of?(String)
|
|
120
|
+
|
|
121
|
+
invalid!("string", value)
|
|
122
|
+
end
|
|
123
|
+
private_class_method :coerce_string
|
|
124
|
+
|
|
125
|
+
def coerce_token(value)
|
|
126
|
+
return Token.new(value) if value.is_a?(String)
|
|
127
|
+
|
|
128
|
+
invalid!("token", value)
|
|
129
|
+
end
|
|
130
|
+
private_class_method :coerce_token
|
|
131
|
+
|
|
132
|
+
def coerce_asset(value)
|
|
133
|
+
return value if value.is_a?(AssetPath)
|
|
134
|
+
return AssetPath.new(value) if value.is_a?(String)
|
|
135
|
+
|
|
136
|
+
invalid!("asset", value)
|
|
137
|
+
end
|
|
138
|
+
private_class_method :coerce_asset
|
|
139
|
+
|
|
140
|
+
def coerce_vector(type_name, value)
|
|
141
|
+
size = VECTOR_TYPES.fetch(type_name)
|
|
142
|
+
invalid!(type_name, value) unless value.is_a?(Array) && value.length == size
|
|
143
|
+
|
|
144
|
+
scalar = type_name.start_with?("int") ? "int" : "double"
|
|
145
|
+
value.map { |element| coerce_scalar(scalar, element) }.freeze
|
|
146
|
+
end
|
|
147
|
+
private_class_method :coerce_vector
|
|
148
|
+
|
|
149
|
+
def coerce_matrix(type_name, value)
|
|
150
|
+
size = MATRIX_TYPES.fetch(type_name)
|
|
151
|
+
invalid!(type_name, value) unless matrix_shape?(value, size)
|
|
152
|
+
|
|
153
|
+
value.map { |row| row.map { |element| coerce_float(element) }.freeze }.freeze
|
|
154
|
+
end
|
|
155
|
+
private_class_method :coerce_matrix
|
|
156
|
+
|
|
157
|
+
def matrix_shape?(value, size)
|
|
158
|
+
value.is_a?(Array) && value.length == size &&
|
|
159
|
+
value.all? { |row| row.is_a?(Array) && row.length == size }
|
|
160
|
+
end
|
|
161
|
+
private_class_method :matrix_shape?
|
|
162
|
+
|
|
163
|
+
def invalid!(type_name, value)
|
|
164
|
+
raise OpenUSD::TypeError, "#{value.inspect} is not a valid #{type_name} value"
|
|
165
|
+
end
|
|
166
|
+
private_class_method :invalid!
|
|
167
|
+
end
|
|
168
|
+
end
|