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,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module OpenUSD
|
|
6
|
+
module Format
|
|
7
|
+
# USDA text format support.
|
|
8
|
+
module Usda
|
|
9
|
+
# Converts USDA text into location-aware tokens.
|
|
10
|
+
class Lexer
|
|
11
|
+
# One lexeme and its source location.
|
|
12
|
+
Token = Data.define(:type, :value, :line, :column, :raw)
|
|
13
|
+
# Single-character grammar symbols.
|
|
14
|
+
SYMBOLS = "(){}[]=,:."
|
|
15
|
+
# Numeric literal pattern shared by ordinary and fast-path scans.
|
|
16
|
+
NUMERIC_LITERAL = /[+-]?(?:(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?|inf|nan)/
|
|
17
|
+
# Supported string escape translations.
|
|
18
|
+
ESCAPES = {
|
|
19
|
+
"n" => "\n", "r" => "\r", "t" => "\t", "b" => "\b",
|
|
20
|
+
"f" => "\f", "\\" => "\\", "\"" => "\""
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
attr_reader :file
|
|
24
|
+
|
|
25
|
+
def initialize(source, file: nil)
|
|
26
|
+
@scanner = StringScanner.new(String(source).delete_prefix("\uFEFF"))
|
|
27
|
+
@file = file
|
|
28
|
+
@line = 1
|
|
29
|
+
@column = 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Iterate through all tokens, including the final EOF token.
|
|
33
|
+
def each_token
|
|
34
|
+
return enum_for(__method__) unless block_given?
|
|
35
|
+
|
|
36
|
+
loop do
|
|
37
|
+
token = next_token
|
|
38
|
+
yield token
|
|
39
|
+
break if token.type == :eof
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Token] next token from the source
|
|
44
|
+
def next_token
|
|
45
|
+
skip_ignored
|
|
46
|
+
return token(:eof, nil, "") if @scanner.eos?
|
|
47
|
+
|
|
48
|
+
return scan_magic if @scanner.match?(/#usda\b/)
|
|
49
|
+
return scan_quoted("\"\"\"", :string) if @scanner.peek(3) == "\"\"\""
|
|
50
|
+
return scan_quoted("\"", :string) if @scanner.peek(1) == "\""
|
|
51
|
+
return scan_quoted("@@@", :asset) if @scanner.peek(3) == "@@@"
|
|
52
|
+
return scan_quoted("@", :asset) if @scanner.peek(1) == "@"
|
|
53
|
+
return scan_path if @scanner.peek(1) == "<"
|
|
54
|
+
|
|
55
|
+
scan_bare_token
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Scan a flat or tuple-grouped numeric array without allocating tokens.
|
|
59
|
+
# @api private
|
|
60
|
+
# @return [Array, nil] values, or nil when the input needs normal lexing
|
|
61
|
+
def scan_numeric_array(component_count: nil)
|
|
62
|
+
rest = @scanner.rest
|
|
63
|
+
closing = rest.index("]")
|
|
64
|
+
return unless closing
|
|
65
|
+
|
|
66
|
+
body = rest[0...closing]
|
|
67
|
+
literals = body.scan(NUMERIC_LITERAL)
|
|
68
|
+
residue = body.gsub(NUMERIC_LITERAL, "").gsub(/[\s,()]/, "")
|
|
69
|
+
return unless residue.empty?
|
|
70
|
+
return if component_count && (literals.length % component_count).nonzero?
|
|
71
|
+
|
|
72
|
+
raw = rest[0..closing]
|
|
73
|
+
@scanner.pos += raw.bytesize
|
|
74
|
+
advance(raw)
|
|
75
|
+
values = literals.map { |literal| numeric_value(literal) }
|
|
76
|
+
component_count ? values.each_slice(component_count).to_a : values
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def scan_bare_token
|
|
82
|
+
return scan_special_number if @scanner.match?(/[+-]?(?:inf|nan)\b/)
|
|
83
|
+
return scan_number if @scanner.match?(/[+-]?(?:\d|\.\d)/)
|
|
84
|
+
return scan_identifier if @scanner.match?(/[\p{L}_]/u)
|
|
85
|
+
return scan_symbol if SYMBOLS.include?(@scanner.peek(1))
|
|
86
|
+
|
|
87
|
+
lexical_error!("unexpected character #{@scanner.peek(1).inspect}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def skip_ignored
|
|
91
|
+
loop do
|
|
92
|
+
skipped = scan_ignored_pattern?(/[ \t\r\n]+/)
|
|
93
|
+
skipped ||= scan_line_comment?("#") unless @scanner.match?(/#usda\b/)
|
|
94
|
+
skipped ||= scan_line_comment?("//")
|
|
95
|
+
skipped ||= scan_block_comment?
|
|
96
|
+
break unless skipped
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def scan_ignored_pattern?(pattern)
|
|
101
|
+
raw = @scanner.scan(pattern)
|
|
102
|
+
return false unless raw
|
|
103
|
+
|
|
104
|
+
advance(raw)
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def scan_line_comment?(prefix)
|
|
109
|
+
return false unless @scanner.peek(prefix.length) == prefix
|
|
110
|
+
|
|
111
|
+
raw = @scanner.scan(/.*(?:\n|\z)/)
|
|
112
|
+
advance(raw)
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def scan_block_comment?
|
|
117
|
+
return false unless @scanner.peek(2) == "/*"
|
|
118
|
+
|
|
119
|
+
start_line = @line
|
|
120
|
+
start_column = @column
|
|
121
|
+
raw = @scanner.scan(%r{/\*.*?\*/}m)
|
|
122
|
+
lexical_error!("unterminated block comment", start_line, start_column) unless raw
|
|
123
|
+
advance(raw)
|
|
124
|
+
true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def scan_magic
|
|
128
|
+
line = @line
|
|
129
|
+
column = @column
|
|
130
|
+
raw = @scanner.scan(/#usda[ \t]+([0-9]+(?:\.[0-9]+)?)/)
|
|
131
|
+
lexical_error!("invalid USDA header", line, column) unless raw
|
|
132
|
+
version = @scanner[1]
|
|
133
|
+
advance(raw)
|
|
134
|
+
Token.new(:magic, version, line, column, raw)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def scan_quoted(delimiter, type)
|
|
138
|
+
line = @line
|
|
139
|
+
column = @column
|
|
140
|
+
raw = consume(delimiter)
|
|
141
|
+
value = +""
|
|
142
|
+
|
|
143
|
+
until @scanner.eos?
|
|
144
|
+
if @scanner.peek(delimiter.length) == delimiter
|
|
145
|
+
raw << consume(delimiter)
|
|
146
|
+
return Token.new(type, value.freeze, line, column, raw.freeze)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
character = consume_character
|
|
150
|
+
raw << character
|
|
151
|
+
if character == "\\" && type == :string
|
|
152
|
+
escaped = consume_character
|
|
153
|
+
raw << escaped
|
|
154
|
+
value << ESCAPES.fetch(escaped, escaped)
|
|
155
|
+
else
|
|
156
|
+
value << character
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
lexical_error!("unterminated #{type}", line, column)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def scan_path
|
|
164
|
+
line = @line
|
|
165
|
+
column = @column
|
|
166
|
+
raw = consume("<")
|
|
167
|
+
value = +""
|
|
168
|
+
|
|
169
|
+
until @scanner.eos?
|
|
170
|
+
character = consume_character
|
|
171
|
+
raw << character
|
|
172
|
+
return Token.new(:path, value.freeze, line, column, raw.freeze) if character == ">"
|
|
173
|
+
|
|
174
|
+
value << character
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
lexical_error!("unterminated path", line, column)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def scan_number
|
|
181
|
+
line = @line
|
|
182
|
+
column = @column
|
|
183
|
+
raw = @scanner.scan(/[+-]?(?:(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?)/)
|
|
184
|
+
value = raw.match?(/[.eE]/) ? Float(raw) : Integer(raw, 10)
|
|
185
|
+
advance(raw)
|
|
186
|
+
Token.new(:number, value, line, column, raw.freeze)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def scan_special_number
|
|
190
|
+
line = @line
|
|
191
|
+
column = @column
|
|
192
|
+
raw = @scanner.scan(/[+-]?(?:inf|nan)\b/)
|
|
193
|
+
value = if raw.include?("nan")
|
|
194
|
+
Float::NAN
|
|
195
|
+
elsif raw.start_with?("-")
|
|
196
|
+
-Float::INFINITY
|
|
197
|
+
else
|
|
198
|
+
Float::INFINITY
|
|
199
|
+
end
|
|
200
|
+
advance(raw)
|
|
201
|
+
Token.new(:number, value, line, column, raw.freeze)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def numeric_value(raw)
|
|
205
|
+
return Float::NAN if raw.include?("nan")
|
|
206
|
+
return raw.start_with?("-") ? -Float::INFINITY : Float::INFINITY if raw.include?("inf")
|
|
207
|
+
|
|
208
|
+
raw.match?(/[.eE]/) ? Float(raw) : Integer(raw, 10)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def scan_identifier
|
|
212
|
+
line = @line
|
|
213
|
+
column = @column
|
|
214
|
+
raw = @scanner.scan(/[\p{L}_][\p{L}\p{N}_:]*(?:\[\])?/u)
|
|
215
|
+
advance(raw)
|
|
216
|
+
Token.new(:identifier, raw.freeze, line, column, raw.freeze)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def scan_symbol
|
|
220
|
+
line = @line
|
|
221
|
+
column = @column
|
|
222
|
+
raw = consume_character
|
|
223
|
+
Token.new(:symbol, raw, line, column, raw)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def consume(expected)
|
|
227
|
+
raw = @scanner.scan(Regexp.new(Regexp.escape(expected)))
|
|
228
|
+
advance(raw)
|
|
229
|
+
raw
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def consume_character
|
|
233
|
+
raw = @scanner.getch
|
|
234
|
+
advance(raw)
|
|
235
|
+
raw
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def advance(raw)
|
|
239
|
+
lines = raw.count("\n")
|
|
240
|
+
if lines.zero?
|
|
241
|
+
@column += raw.length
|
|
242
|
+
else
|
|
243
|
+
@line += lines
|
|
244
|
+
@column = raw.length - raw.rindex("\n")
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def token(type, value, raw)
|
|
249
|
+
Token.new(type, value, @line, @column, raw)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def lexical_error!(message, line = @line, column = @column)
|
|
253
|
+
raise ParseError.new(message, file: file, line: line, column: column)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
module Format
|
|
5
|
+
module Usda
|
|
6
|
+
# Recursive-descent parser for USDA layers.
|
|
7
|
+
class Parser
|
|
8
|
+
# Authored prim specifier keywords.
|
|
9
|
+
SPECIFIERS = %w[def over class].freeze
|
|
10
|
+
# Recognized list-edit operation keywords.
|
|
11
|
+
LIST_OPERATIONS = %w[prepend append delete reorder add explicit].freeze
|
|
12
|
+
# Recognized property qualifiers.
|
|
13
|
+
QUALIFIERS = %w[custom uniform varying].freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Parse a USDA string.
|
|
17
|
+
# @return [Layer]
|
|
18
|
+
def parse(source, file: nil)
|
|
19
|
+
new(source, file: file).parse
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :file
|
|
24
|
+
|
|
25
|
+
def initialize(source, file: nil)
|
|
26
|
+
@lexer = Lexer.new(source, file: file)
|
|
27
|
+
@file = file
|
|
28
|
+
advance
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Parse the configured source.
|
|
32
|
+
# @return [Layer]
|
|
33
|
+
def parse
|
|
34
|
+
header = expect(:magic, description: "USDA header")
|
|
35
|
+
error!("unsupported USDA version #{header.value}") unless header.value == "1.0"
|
|
36
|
+
|
|
37
|
+
layer = Layer.new(file)
|
|
38
|
+
layer.metadata.merge!(parse_metadata_block(layer_comment: true)) if symbol?("(")
|
|
39
|
+
layer.add_root_prim(parse_prim) until current.type == :eof
|
|
40
|
+
layer
|
|
41
|
+
rescue OpenUSD::PathError, OpenUSD::TypeError => e
|
|
42
|
+
error!(e.message)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_reader :current
|
|
48
|
+
|
|
49
|
+
def parse_prim
|
|
50
|
+
specifier = expect_identifier
|
|
51
|
+
error!("expected prim specifier") unless SPECIFIERS.include?(specifier)
|
|
52
|
+
|
|
53
|
+
type_name = expect_identifier unless current.type == :string
|
|
54
|
+
name = expect(:string, description: "prim name").value
|
|
55
|
+
metadata = symbol?("(") ? parse_metadata_block : {}
|
|
56
|
+
references, reference_list_op, references_authored = ReferenceMetadata.extract(metadata)
|
|
57
|
+
prim = PrimSpec.new(
|
|
58
|
+
name,
|
|
59
|
+
type_name: type_name,
|
|
60
|
+
specifier: specifier,
|
|
61
|
+
metadata: metadata,
|
|
62
|
+
references: references_authored ? references : PrimSpec::REFERENCES_UNAUTHORED,
|
|
63
|
+
reference_list_op: reference_list_op
|
|
64
|
+
)
|
|
65
|
+
parse_prim_body(prim)
|
|
66
|
+
prim
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_prim_body(prim)
|
|
70
|
+
expect_symbol("{")
|
|
71
|
+
until accept_symbol?("}")
|
|
72
|
+
if specifier?
|
|
73
|
+
prim.add_child(parse_prim)
|
|
74
|
+
elsif identifier?("variantSet")
|
|
75
|
+
parse_variant_set(prim)
|
|
76
|
+
else
|
|
77
|
+
PropertyMerger.merge(prim, parse_property)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def parse_variant_set(prim)
|
|
83
|
+
advance
|
|
84
|
+
name = expect(:string, description: "variant set name").value
|
|
85
|
+
expect_symbol("=")
|
|
86
|
+
expect_symbol("{")
|
|
87
|
+
choices = {}
|
|
88
|
+
until accept_symbol?("}")
|
|
89
|
+
choice = expect(:string, description: "variant name").value
|
|
90
|
+
holder = PrimSpec.new("_variant")
|
|
91
|
+
parse_prim_body(holder)
|
|
92
|
+
choices[choice] = Variant.new(holder.properties.freeze, holder.children.freeze)
|
|
93
|
+
end
|
|
94
|
+
prim.variant_sets[name] = choices
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_property
|
|
98
|
+
qualifiers = []
|
|
99
|
+
qualifiers << consume_identifier while current.type == :identifier && QUALIFIERS.include?(current.value)
|
|
100
|
+
|
|
101
|
+
return parse_relationship(qualifiers) if identifier?("rel")
|
|
102
|
+
|
|
103
|
+
type_name = expect_identifier
|
|
104
|
+
name = expect_identifier
|
|
105
|
+
suffix = parse_property_suffix
|
|
106
|
+
value = parse_assigned_value(type_name)
|
|
107
|
+
metadata = symbol?("(") ? parse_metadata_block : {}
|
|
108
|
+
build_attribute(name, type_name, suffix, value, qualifiers, metadata)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_relationship(qualifiers)
|
|
112
|
+
advance
|
|
113
|
+
name = expect_identifier
|
|
114
|
+
value = accept_symbol?("=") ? parse_value : RelationshipSpec::UNAUTHORED
|
|
115
|
+
metadata = symbol?("(") ? parse_metadata_block : {}
|
|
116
|
+
targets = value.equal?(RelationshipSpec::UNAUTHORED) ? value : unwrap_list(value).compact
|
|
117
|
+
RelationshipSpec.new(name, targets: targets, custom: qualifiers.include?("custom"), metadata: metadata)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_property_suffix
|
|
121
|
+
return unless accept_symbol?(".")
|
|
122
|
+
|
|
123
|
+
expect_identifier
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_assigned_value(type_name)
|
|
127
|
+
return AttributeSpec::UNAUTHORED unless accept_symbol?("=")
|
|
128
|
+
|
|
129
|
+
parse_value(type_name)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def build_attribute(name, type_name, suffix, value, qualifiers, metadata)
|
|
133
|
+
attribute = AttributeSpec.new(
|
|
134
|
+
name, type_name,
|
|
135
|
+
variability: qualifiers.include?("uniform") ? :uniform : :varying,
|
|
136
|
+
custom: qualifiers.include?("custom"),
|
|
137
|
+
metadata: metadata
|
|
138
|
+
)
|
|
139
|
+
apply_attribute_value(attribute, suffix, value)
|
|
140
|
+
attribute
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def apply_attribute_value(attribute, suffix, value)
|
|
144
|
+
case suffix
|
|
145
|
+
when "timeSamples"
|
|
146
|
+
attribute.time_samples = value
|
|
147
|
+
when "connect"
|
|
148
|
+
attribute.connections = unwrap_list(value)
|
|
149
|
+
when nil
|
|
150
|
+
attribute.set(value) unless value.equal?(AttributeSpec::UNAUTHORED)
|
|
151
|
+
else
|
|
152
|
+
attribute.metadata[suffix] = value
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_metadata_block(layer_comment: false)
|
|
157
|
+
expect_symbol("(")
|
|
158
|
+
metadata = {}
|
|
159
|
+
until accept_symbol?(")")
|
|
160
|
+
if layer_comment && current.type == :string
|
|
161
|
+
error!("duplicate layer comment") if metadata.key?("comment")
|
|
162
|
+
|
|
163
|
+
metadata["comment"] = consume.value
|
|
164
|
+
else
|
|
165
|
+
metadata.merge!(parse_metadata_entry)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
metadata
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def parse_metadata_entry
|
|
172
|
+
operation = consume_identifier if current.type == :identifier && LIST_OPERATIONS.include?(current.value)
|
|
173
|
+
first = expect_identifier
|
|
174
|
+
type_name, key = metadata_type_and_key(first)
|
|
175
|
+
expect_symbol("=")
|
|
176
|
+
value = parse_value(type_name)
|
|
177
|
+
value = ListOp.new(operation, value) if operation
|
|
178
|
+
{ key => value }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def metadata_type_and_key(first)
|
|
182
|
+
return [nil, first] unless current.type == :identifier
|
|
183
|
+
|
|
184
|
+
[first, consume_identifier]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def parse_value(expected_type = nil)
|
|
188
|
+
case current.type
|
|
189
|
+
when :string, :number then consume.value
|
|
190
|
+
when :asset then parse_asset
|
|
191
|
+
when :path then Path.parse(consume.value)
|
|
192
|
+
when :identifier then parse_identifier_value
|
|
193
|
+
when :symbol then parse_compound_value(expected_type)
|
|
194
|
+
else error!("expected a value")
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def parse_asset
|
|
199
|
+
asset = AssetPath.new(consume.value)
|
|
200
|
+
return asset unless current.type == :path
|
|
201
|
+
|
|
202
|
+
Reference.new(asset, Path.parse(consume.value))
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def parse_identifier_value
|
|
206
|
+
value = consume.value
|
|
207
|
+
return true if value == "true"
|
|
208
|
+
return false if value == "false"
|
|
209
|
+
return nil if value == "None"
|
|
210
|
+
return Float::INFINITY if value == "inf"
|
|
211
|
+
return -Float::INFINITY if value == "-inf"
|
|
212
|
+
return Float::NAN if value == "nan"
|
|
213
|
+
|
|
214
|
+
Token.new(value)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_compound_value(expected_type)
|
|
218
|
+
return parse_sequence("(", ")", expected_type) if symbol?("(")
|
|
219
|
+
return parse_array(expected_type) if symbol?("[")
|
|
220
|
+
return parse_dictionary if symbol?("{")
|
|
221
|
+
|
|
222
|
+
error!("expected a value")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def parse_array(expected_type)
|
|
226
|
+
base_type = Types.base_type(expected_type.to_s)
|
|
227
|
+
component_count = Types::VECTOR_TYPES[base_type]
|
|
228
|
+
numeric = component_count || Types::FLOAT_TYPES.include?(base_type) ||
|
|
229
|
+
Types::INTEGER_RANGES.key?(base_type)
|
|
230
|
+
return parse_sequence("[", "]", base_type) unless numeric
|
|
231
|
+
|
|
232
|
+
values = @lexer.scan_numeric_array(component_count: component_count)
|
|
233
|
+
return parse_sequence("[", "]", base_type) unless values
|
|
234
|
+
|
|
235
|
+
advance
|
|
236
|
+
values
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def parse_sequence(opening, closing, expected_type)
|
|
240
|
+
expect_symbol(opening)
|
|
241
|
+
values = []
|
|
242
|
+
until accept_symbol?(closing)
|
|
243
|
+
values << parse_value(expected_type)
|
|
244
|
+
accept_symbol?(",")
|
|
245
|
+
end
|
|
246
|
+
values
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def parse_dictionary
|
|
250
|
+
expect_symbol("{")
|
|
251
|
+
values = {}
|
|
252
|
+
until accept_symbol?("}")
|
|
253
|
+
key, type_name, separator = parse_dictionary_key
|
|
254
|
+
expect_symbol(separator)
|
|
255
|
+
values[key] = parse_value(type_name)
|
|
256
|
+
accept_symbol?(",")
|
|
257
|
+
end
|
|
258
|
+
values
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def parse_dictionary_key
|
|
262
|
+
return [consume.value, nil, ":"] if current.type == :number
|
|
263
|
+
|
|
264
|
+
first = consume
|
|
265
|
+
error!("expected dictionary key") unless %i[string identifier].include?(first.type)
|
|
266
|
+
return [first.value, nil, current.value] if symbol?(":") || symbol?("=")
|
|
267
|
+
|
|
268
|
+
[expect_identifier, first.value, "="]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def unwrap_list(value)
|
|
272
|
+
value = value.value if value.is_a?(ListOp)
|
|
273
|
+
value.is_a?(Array) ? value : [value]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def specifier? = current.type == :identifier && SPECIFIERS.include?(current.value)
|
|
277
|
+
|
|
278
|
+
def identifier?(value) = current.type == :identifier && current.value == value
|
|
279
|
+
|
|
280
|
+
def symbol?(value) = current.type == :symbol && current.value == value
|
|
281
|
+
|
|
282
|
+
def accept_symbol?(value)
|
|
283
|
+
return false unless symbol?(value)
|
|
284
|
+
|
|
285
|
+
advance
|
|
286
|
+
true
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def expect_symbol(value)
|
|
290
|
+
expect(:symbol, value: value, description: value)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def expect_identifier
|
|
294
|
+
expect(:identifier, description: "identifier").value
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def consume_identifier
|
|
298
|
+
expect_identifier
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def expect(type, value: nil, description: type)
|
|
302
|
+
token = current
|
|
303
|
+
matches = token.type == type && (value.nil? || token.value == value)
|
|
304
|
+
error!("expected #{description}, got #{token.raw.inspect}") unless matches
|
|
305
|
+
advance
|
|
306
|
+
token
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def consume
|
|
310
|
+
token = current
|
|
311
|
+
advance
|
|
312
|
+
token
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def advance
|
|
316
|
+
@current = @lexer.next_token
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def error!(message)
|
|
320
|
+
raise ParseError.new(message, file: file, line: current.line, column: current.column)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# File reader adapter for the format registry.
|
|
325
|
+
module Reader
|
|
326
|
+
module_function
|
|
327
|
+
|
|
328
|
+
# Read a USDA layer from a filesystem path.
|
|
329
|
+
# @return [Layer]
|
|
330
|
+
def read(path)
|
|
331
|
+
Parser.parse(File.binread(path).force_encoding(Encoding::UTF_8), file: path)
|
|
332
|
+
rescue Errno::ENOENT
|
|
333
|
+
raise CompositionError, "layer not found: #{path}"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
module Format
|
|
5
|
+
module Usda
|
|
6
|
+
# Combines fields from repeated USDA declarations into one property spec.
|
|
7
|
+
module PropertyMerger
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Merge a parsed property into its owning prim.
|
|
11
|
+
# @return [AttributeSpec, RelationshipSpec]
|
|
12
|
+
def merge(prim, property)
|
|
13
|
+
existing = prim.property_named(property.name)
|
|
14
|
+
return prim.add_property(property) unless existing
|
|
15
|
+
|
|
16
|
+
unless existing.instance_of?(property.class)
|
|
17
|
+
raise OpenUSD::TypeError, "property #{property.name} is declared with conflicting kinds"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
existing.is_a?(AttributeSpec) ? merge_attribute(existing, property) : merge_relationship(existing, property)
|
|
21
|
+
existing
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def merge_attribute(existing, property)
|
|
25
|
+
unless existing.type_name == property.type_name
|
|
26
|
+
raise OpenUSD::TypeError, "attribute #{property.name} is declared with conflicting types"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
existing.custom ||= property.custom
|
|
30
|
+
existing.variability = :uniform if property.variability == :uniform
|
|
31
|
+
existing.metadata.merge!(property.metadata)
|
|
32
|
+
existing.set(property.default) if property.default_authored?
|
|
33
|
+
existing.time_samples = property.time_samples if property.time_samples_authored?
|
|
34
|
+
existing.connections = property.connections if property.connections_authored?
|
|
35
|
+
end
|
|
36
|
+
private_class_method :merge_attribute
|
|
37
|
+
|
|
38
|
+
def merge_relationship(existing, property)
|
|
39
|
+
existing.custom ||= property.custom
|
|
40
|
+
existing.metadata.merge!(property.metadata)
|
|
41
|
+
existing.targets = property.targets if property.targets_authored?
|
|
42
|
+
end
|
|
43
|
+
private_class_method :merge_relationship
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUSD
|
|
4
|
+
module Format
|
|
5
|
+
module Usda
|
|
6
|
+
# Converts a USDA references metadata field to model references.
|
|
7
|
+
module ReferenceMetadata
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Remove and normalize references metadata.
|
|
11
|
+
# @return [Array(Array<Reference>, Symbol, Boolean)]
|
|
12
|
+
def extract(metadata)
|
|
13
|
+
return [[], nil, false] unless metadata.key?("references")
|
|
14
|
+
|
|
15
|
+
value = metadata.delete("references")
|
|
16
|
+
operation = value.operation if value.is_a?(ListOp)
|
|
17
|
+
value = value.value if value.is_a?(ListOp)
|
|
18
|
+
references = Array(unwrap_list(value)).map { |reference| normalize(reference) }
|
|
19
|
+
[references, operation, true]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def normalize(reference)
|
|
23
|
+
return Reference.internal(reference) if reference.is_a?(Path)
|
|
24
|
+
return reference if reference.is_a?(Reference)
|
|
25
|
+
|
|
26
|
+
Reference.new(reference)
|
|
27
|
+
end
|
|
28
|
+
private_class_method :normalize
|
|
29
|
+
|
|
30
|
+
def unwrap_list(value)
|
|
31
|
+
value.is_a?(Array) ? value : [value]
|
|
32
|
+
end
|
|
33
|
+
private_class_method :unwrap_list
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|