Merge: Popcorn templates
authorJean Privat <jean@pryen.org>
Thu, 25 May 2017 14:55:23 +0000 (10:55 -0400)
committerJean Privat <jean@pryen.org>
Thu, 25 May 2017 14:55:23 +0000 (10:55 -0400)
# Template rendering for popcorn

## Basic templates

Use TemplateString to render really basic templates that just need macro
replacements.

Example:
~~~nit
class TemplateStringHandler
        super Handler
redef fun get(req, res) do
# Values to add in the template
var values = new HashMap[String, String]
values["USER"] = "Morriar"
values["MYSITE"] = "My super website"

# Render it with a shortcut
res.template_string("""
<h1>Hello %USER%!</h1>
<p>Welcome to %MYSITE%.</p>
""", values)
end
 end
 ~~~

 For larger templates, you can also use external files (makes your Nit code cleaner):
 ~~~nit
 class TemplateFileHandler
super Handler

redef fun get(req, res) do
# Values to add in the template
var values = new HashMap[String, String]
values["USER"] = "Morriar"
values["MYSITE"] = "My super website"

# Render it from an external file
res.template_file("example_template.tpl", values)
end
 end
 ~~~

 ## Using pug templates

 Pug is a templating format provided by the external command `pug`.
 For complex templates that need conditional or loop statements, pug can be a solution.

 See the pug syntax here: https://pugjs.org/api/getting-started.html

 ~~~nit
 class PugFileHandler
super Handler

redef fun get(req, res) do
# Values to add in the template
var json = new JsonObject
json["user"] = "Morriar"
json["mysite"] = "My super website"

# Render it from an external file
res.pug_file("example_template.pug", json)
end
 end
 ~~~

Pull-Request: #2449
Reviewed-by: Jean Privat <jean@pryen.org>

35 files changed:
contrib/model_viewer/src/globe.nit
contrib/model_viewer/src/model_viewer.nit
examples/rosettacode/align_columns.nit
examples/rosettacode/integer_overflow.nit [new file with mode: 0644]
lib/core/collection/abstract_collection.nit
lib/core/stream.nit
lib/core/text/string_search.nit
lib/dom/dom.nit
lib/gamnit/bmfont.nit [new file with mode: 0644]
lib/gamnit/depth/depth_core.nit
lib/gamnit/depth/more_materials.nit
lib/gamnit/depth/more_meshes.nit
lib/gamnit/depth/more_models.nit
lib/gamnit/depth/selection.nit
lib/gamnit/flat.nit
lib/gamnit/font.nit [new file with mode: 0644]
lib/gamnit/textures.nit
lib/gamnit/tileset.nit
lib/geometry/points_and_lines.nit
lib/more_collections.nit
lib/opts.nit
lib/popcorn/pop_tasks.nit [new file with mode: 0644]
lib/popcorn/pop_validation.nit
lib/popcorn/popcorn.nit
src/compiler/coloring.nit
src/compiler/test_coloring.nit [new file with mode: 0644]
src/metrics/metrics_base.nit
src/model/model.nit
src/parser/parser_nodes.nit
src/rapid_type_analysis.nit
src/vm/virtual_machine.nit
tests/base_ret_covar_int.nit
tests/example_sorter.nit
tests/sav/base_virtual_type_self_alt2.res
tests/sav/integer_overflow.res [new file with mode: 0644]

index 5ad0407..8fe1158 100644 (file)
@@ -63,13 +63,13 @@ class GlobeModel
        redef fun load
        do
                leaves.add new LeafModel(
-                       new Mesh.uv_sphere(1.0, 2*n_parallels, n_parallels),
+                       new UVSphere(1.0, 2*n_parallels, n_parallels),
                        new GlobeMaterial.surface)
                leaves.add new LeafModel(
-                       new Mesh.uv_sphere(1.1, 2*n_parallels, n_parallels),
+                       new UVSphere(1.1, 2*n_parallels, n_parallels),
                        new GlobeMaterial.clouds)
                leaves.add new LeafModel(
-                       new Mesh.uv_sphere(1.2, 2*n_parallels, n_parallels),
+                       new UVSphere(1.2, 2*n_parallels, n_parallels),
                        new GlobeMaterial.atmo)
        end
 end
index a79fdf9..9353ef5 100644 (file)
@@ -30,9 +30,9 @@ redef class App
 
        # All available models
        var models: Array[Model] = [
-               new LeafModel(new Cube, new SmoothMaterial.default),
-               new LeafModel(new Mesh.uv_sphere(4.0, 32, 16), new SmoothMaterial.default),
-               new LeafModel(new Mesh.uv_sphere(4.0, 32, 16), new NormalsMaterial),
+               new LeafModel(new Cube, new Material),
+               new LeafModel(new UVSphere(4.0, 32, 16), new Material),
+               new LeafModel(new UVSphere(4.0, 32, 16), new NormalsMaterial),
                new Model("models/Tree_01.obj"),
                new Model("models/Oak_Fall_01.obj"),
                new Model("models/Quandtum_BA-2_v1_1.obj"),
index edbee46..d7602a7 100644 (file)
@@ -6,7 +6,7 @@
 # Task: Align columns
 # SEE: <http://rosettacode.org/wiki/Align_columns>
 #
-# Use `Text::justify` for the standard library.
+# Uses `Text::justify` from the standard library.
 module align_columns
 
 fun aligner(text: String, left: Float)
diff --git a/examples/rosettacode/integer_overflow.nit b/examples/rosettacode/integer_overflow.nit
new file mode 100644 (file)
index 0000000..9d73cc6
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/env nit
+#
+# This file is part of NIT ( http://www.nitlanguage.org ).
+# This program is public domain
+
+# Task: Integer Overflow
+# SEE: <rosettacode.org/wiki/Integer_overflow>
+module integer_overflow
+
+# Nit possed integers of 32-bit name Int32 and unsigned
+# integers of 32-bit.
+# Nit does not recognize overflow.
+
+print "Signed 32-bit:"
+print( -(-2147483647i32 - 1i32) )
+print 2000000000i32 + 2000000000i32
+print -2147483647i32 - 2147483647i32
+print 46341i32 * 46341i32
+#print( (-2147483647i32 - 1i32) / -1i32 )
+
+print "unsigned 32-bit:"
+print -4294967295u32
+print 3000000000u32 + 3000000000u32
+print 2147483647u32 - 4294967295u32
+print 65537u32 * 65537u32
index 5ed911f..309d048 100644 (file)
@@ -385,7 +385,7 @@ end
 interface SimpleCollection[E]
        super RemovableCollection[E]
 
-       # Add an item in a collection.
+       # Add `item` to this collection.
        #
        #     var a = [1,2]
        #     a.add 3
index 6fbc63c..80b96f0 100644 (file)
@@ -55,7 +55,7 @@ abstract class Stream
        # Used to inform `self` that the operations are over.
        # Specific streams can use this to free some resources.
        #
-       # Is automatically invoked at the end of `woth` structures.
+       # Is automatically invoked at the end of `with` structures.
        #
        # call `close` by default.
        fun finish do close
index edd90db..6d6302e 100644 (file)
@@ -470,11 +470,14 @@ redef class Text
                return res
        end
 
-       # Replace all occurences of `pattern` with `string`
+       # Replace all occurrences of `pattern` with `string`
        #
-       #     assert "hlelo".replace("le", "el")             ==  "hello"
-       #     assert "hello".replace('l', "")        ==  "heo"
-       fun replace(pattern: Pattern, string: SELFTYPE): String
+       #     assert "hlelo".replace("le", "el") == "hello"
+       #     assert "hello".replace('l', "")    == "heo"
+       #
+       #     var t: Text = "hello"
+       #     assert t.replace("hello", new FlatBuffer) == ""
+       fun replace(pattern: Pattern, string: Text): String
        do
                return self.split_with(pattern).join(string)
        end
index 786bdcc..5a1b8c0 100644 (file)
@@ -30,9 +30,9 @@ redef class XMLEntity
        # assert xml["animal"].length == 1
        # assert xml["animal"].first["cat"].length == 2
        # ~~~
-       fun [](tag_name: String): Array[XMLEntity]
+       fun [](tag_name: String): Array[XMLTag]
        do
-               var res = new Array[XMLEntity]
+               var res = new Array[XMLTag]
                for child in children do
                        if child isa XMLTag and child.tag_name == tag_name then
                                res.add child
@@ -67,3 +67,27 @@ redef class XMLStartTag
                abort
        end
 end
+
+redef class XMLAttrTag
+
+       # Attributes as a map (ignoring malformed attributes)
+       #
+       # ~~~
+       # var xml = """
+       # <student first="Snow" last="Man"/>
+       # """.to_xml
+       #
+       # var attributes = xml["student"].first.as(XMLAttrTag).attributes_to_map
+       # assert attributes.join(", ", ":") == "first:Snow, last:Man"
+       # ~~~
+       fun attributes_to_map: Map[String, String]
+       do
+               var m = new Map[String, String]
+               for a in attributes do
+                       if a isa XMLStringAttr then
+                               m[a.name] = a.value
+                       end
+               end
+               return m
+       end
+end
diff --git a/lib/gamnit/bmfont.nit b/lib/gamnit/bmfont.nit
new file mode 100644 (file)
index 0000000..4abdaa5
--- /dev/null
@@ -0,0 +1,422 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Parse Angel Code BMFont format and draw text
+#
+# The BMFont format supports packed textures, varying advance per character and
+# even kernings. It can be generated with a number of tools, inluding:
+# * BMFont, free software Windows app, http://www.angelcode.com/products/bmfont/
+# * Littera, a web app, http://kvazars.com/littera/
+#
+# Format reference: http://www.angelcode.com/products/bmfont/doc/file_format.html
+module bmfont
+
+private import dom
+
+import font
+
+# BMFont description, parsed with `Text::parse_bmfont` or loaded as a `BMFontAsset`
+#
+# This class flattens all the `info` and `common` data.
+class BMFont
+
+       # ---
+       # info part
+       #
+       # How the font was generated.
+
+       # Name of the source true type font
+       var face: Text
+
+       # Size of the source true type font
+       var size: Int
+
+       # Is the font bold?
+       var bold: Bool
+
+       # Is the font italic?
+       var italic: Bool
+
+       # Does the font uses the Unicode charset?
+       var unicode: Bool
+
+       # Padding for each character
+       #
+       # In the format `up,right,down,left`
+       var padding: String
+
+       # Spacing for each character
+       #
+       # In the format `horizontal,vertical`.
+       var spacing: String
+
+       # ---
+       # common part
+       #
+       # Information common to all characters
+
+       # Distance in pixels between each line of text
+       var line_height: Int
+
+       # Pixels from the top of the line to the base of the characters
+       var base: Int
+
+       # Width of the texture
+       var scale_w: Int
+
+       # Height of the texture
+       var scale_h: Int
+
+       # Textures
+       var pages = new Map[String, TextureAsset]
+
+       # Characters in the font
+       var chars = new Map[Char, BMFontChar]
+
+       # Distance between certain characters
+       var kernings = new HashMap2[Char, Char, Int]
+
+       redef fun to_s do return "<{class_name} {face} at {size} pt, "+
+                                "{pages.length} pages, {chars.length} chars>"
+
+       # TODO
+       #
+       # # From info
+       # charset
+       # stretchH
+       # smooth
+       # aa
+       # outline
+       #
+       # # From common
+       # packed
+       # alphaChnl
+       # redChnl
+       # greenChnl
+       # blueChnl
+end
+
+# Description of a character in a `BMFont`
+class BMFontChar
+
+       # Subtexture left coordinate
+       var x: Int
+
+       # Subtexture top coordinate
+       var y: Int
+
+       # Subtexture width
+       var width: Int
+
+       # Subtexture height
+       var height: Int
+
+       # Drawing offset on X
+       var xoffset: Int
+
+       # Drawing offset on Y
+       var yoffset: Int
+
+       # Cursor advance after drawing this character
+       var xadvance: Int
+
+       # Full texture contaning this character and others
+       var page: TextureAsset
+
+       # TODO Channel where the image is found
+       #var chnl: Int
+
+       # Subtexture with this character image only
+       var subtexture: Texture = page.subtexture(x, y, width, height) is lazy
+end
+
+redef class Text
+
+       # Parse `self` as an XML BMFont description file
+       #
+       # Reports only basic XML format errors, other errors may be ignored or
+       # cause a crash.
+       #
+       # ~~~
+       # var desc = """
+       # <font>
+       #   <info face="arial" size="72" bold="0" italic="0" charset=""
+       #         unicode="1" stretchH="100" smooth="1" aa="1" padding="2,2,2,2"
+       #         spacing="0,0" outline="0"/>
+       #   <common lineHeight="80" base="65" scaleW="4030" scaleH="231"
+       #           pages="1" packed="0"/>
+       #   <pages>
+       #     <page id="0" file="arial.png"/>
+       #   </pages>
+       #   <chars count="3">
+       #     <char id="65" x="2519" y="10" width="55" height="59" xoffset="0"
+       #           yoffset="13" xadvance="48" page="0" chnl="15"/>
+       #     <char id="66" x="2600" y="10" width="46" height="58" xoffset="5"
+       #           yoffset="13" xadvance="48" page="0" chnl="15"/>
+       #     <char id="67" x="2673" y="9" width="52" height="60" xoffset="4"
+       #           yoffset="12" xadvance="52" page="0" chnl="15"/>
+       #   </chars>
+       #   <kernings count="1">
+       #     <kerning first="65" second="67" amount="-1"/>
+       #   </kernings>
+       # </font>
+       # """
+       #
+       # var fnt = desc.parse_bmfont("dir_in_assets").value
+       # assert fnt.to_s == "<BMFont arial at 72 pt, 1 pages, 3 chars>"
+       # assert fnt.line_height == 80
+       # assert fnt.kernings['A', 'C'] == -1
+       # assert fnt.chars['A'].page.path == "dir_in_assets/arial.png"
+       # ~~~
+       fun parse_bmfont(dir: String): MaybeError[BMFont, Error]
+       do
+               # Parse XML
+               var xml = to_xml
+               if xml isa XMLError then
+                       var msg = "XML Parse Error: {xml.message}:{xml.location or else 0}"
+                       return new MaybeError[BMFont, Error](maybe_error=new Error(msg))
+               end
+
+               # Basic sanity check
+               var roots = xml["font"]
+               if roots.is_empty then
+                       var msg = "Error: the XML document doesn't declare the expected `font` root"
+                       return new MaybeError[BMFont, Error](maybe_error=new Error(msg))
+               end
+
+               # Expect the rest of the document to be well formatted
+               var root = roots.first
+
+               var info = root["info"].first
+               assert info isa XMLAttrTag
+               var info_map = info.attributes_to_map
+
+               var common = root["common"].first
+               assert common isa XMLAttrTag
+               var common_map = common.attributes_to_map
+
+               var fnt = new BMFont(
+                       info_map["face"],
+                       info_map["size"].to_i,
+                       info_map["bold"] == "1",
+                       info_map["italic"] == "1",
+                       info_map["unicode"] == "1",
+                       info_map["padding"],
+                       info_map["spacing"],
+                       common_map["lineHeight"].to_i,
+                       common_map["base"].to_i,
+                       common_map["scaleW"].to_i,
+                       common_map["scaleH"].to_i
+               )
+
+               # Pages / pixel data files
+               var xml_pages = root["pages"].first
+               for page in xml_pages["page"] do
+                       if not page isa XMLAttrTag then continue
+
+                       var attributes = page.attributes_to_map
+                       var file = dir / attributes["file"]
+                       fnt.pages[attributes["id"]] = new TextureAsset(file)
+               end
+
+               # Char description
+               for item in root["chars"].first["char"] do
+                       if not item isa XMLAttrTag then continue
+
+                       var attributes = item.attributes_to_map
+                       var id = attributes["id"].to_i.code_point
+
+                       var c = new BMFontChar(
+                               attributes["x"].to_i, attributes["y"].to_i,
+                               attributes["width"].to_i, attributes["height"].to_i,
+                               attributes["xoffset"].to_i, attributes["yoffset"].to_i,
+                               attributes["xadvance"].to_i,
+                               fnt.pages[attributes["page"]])
+
+                       fnt.chars[id] = c
+               end
+
+               # Kerning between two characters
+               var kernings = root["kernings"]
+               if kernings.not_empty then
+                       for item in kernings.first["kerning"] do
+                               if not item isa XMLAttrTag then continue
+
+                               var attributes = item.attributes_to_map
+                               var first = attributes["first"].to_i.code_point
+                               var second = attributes["second"].to_i.code_point
+                               var amount = attributes["amount"].to_i
+                               fnt.kernings[first, second] = amount
+                       end
+               end
+
+               return new MaybeError[BMFont, Error](fnt)
+       end
+end
+
+# BMFont from the assets folder
+#
+# ~~~
+# redef class App
+#     var font = new BMFontAsset("arial.fnt")
+#     var ui_text = new TextSprites(font, ui_camera.top_left)
+#
+#     redef fun on_create
+#     do
+#         super
+#
+#         font.load
+#         assert font.error == null
+#
+#         ui_text.text = "Hello world!"
+#     end
+# end
+# ~~~
+class BMFontAsset
+       super Asset
+       super Font
+
+       # Font description
+       #
+       # Require: `error == null`
+       fun desc: BMFont
+       do
+               # Cached results
+               var cache = desc_cache
+               if cache != null then return cache
+               var error = error
+               assert error == null else print_error error
+
+               # Load on first access
+               load
+               error = self.error
+               assert error == null else print_error error
+
+               return desc_cache.as(not null)
+       end
+
+       private var desc_cache: nullable BMFont = null
+
+       # Error at loading
+       var error: nullable Error = null
+
+       # XML description in the assets folder
+       private var text_asset = new TextAsset(path) is lateinit
+
+       # Load font description and textures from the assets folder
+       #
+       # Sets `error` if an error occurred, otherwise
+       # the font description can be accessed via `desc`.
+       fun load
+       do
+               var text_asset = text_asset
+               text_asset.load
+               var error = text_asset.error
+               if error != null then
+                       self.error = error
+                       return
+               end
+
+               var desc = text_asset.to_s
+               var fnt_or_error = desc.parse_bmfont(path.dirname)
+               if fnt_or_error.is_error then
+                       self.error = fnt_or_error.error
+                       return
+               end
+
+               var fnt = fnt_or_error.value
+               self.desc_cache = fnt
+
+               # Load textures too
+               for page_name, texture in fnt.pages do
+                       texture.load
+
+                       # Move up any texture loading error.
+                       # This algo keeps only the latest error,
+                       # but this isn't a problem on single page fonts.
+                       error = texture.error
+                       if error != null then self.error = error
+               end
+       end
+
+       redef fun write_into(text_sprites, text)
+       do
+               var dx = 0.0
+               var dy = 0.0
+
+               var line_height = desc.line_height.to_f
+
+               var prev_char = null
+               for c in text do
+
+                       var partial_line_mod = 0.4
+
+                       # Special characters
+                       if c == '\n' then
+                               dy -= line_height.to_f
+                               dx = 0.0 #advance/2.0
+                               prev_char = null
+                               continue
+                       else if c == pld then
+                               dy -= line_height * partial_line_mod.to_f
+                               prev_char = null
+                               continue
+                       else if c == plu then
+                               dy += line_height * partial_line_mod.to_f
+                               prev_char = null
+                               continue
+                       else if c.is_whitespace then
+                               var advance = if desc.chars.keys.has(' ') then
+                                               desc.chars[' '].xadvance.to_f
+                                       else if desc.chars.keys.has('f') then
+                                               desc.chars['f'].xadvance.to_f
+                                       else 16.0
+                               dx += advance
+                               prev_char = null
+                               continue
+                       end
+
+                       # Replace or skip unknown characters
+                       if not desc.chars.keys.has(c) then
+                               var rc = replacement_char
+                               if rc == null then continue
+                               c = rc
+                       end
+
+                       var char_info = desc.chars[c]
+                       var advance = char_info.xadvance.to_f
+
+                       var kerning = 0.0
+                       if prev_char != null then
+                               kerning = (desc.kernings[prev_char, c] or else 0).to_f
+                       end
+
+                       var x = dx + char_info.width.to_f/2.0  + char_info.xoffset.to_f + kerning
+                       var y = dy - char_info.height.to_f/2.0 - char_info.yoffset.to_f
+                       var pos = text_sprites.anchor.offset(x, y, 0.0)
+                       text_sprites.sprites.add new Sprite(char_info.subtexture, pos)
+
+                       dx += advance + kerning
+                       prev_char = c
+               end
+       end
+
+       # Character replacing other charactesr missing from the font
+       private var replacement_char: nullable Char is lazy do
+               for c in  "�?".chars do
+                       if desc.chars.keys.has(c) then return c
+               end
+               return null
+       end
+end
index 45863dc..994e8be 100644 (file)
@@ -17,10 +17,30 @@ module depth_core
 
 intrude import gamnit::flat
 
-# Visible entity in the game world, represented by its `model` modified by the other attributes
+# Visible 3D entity in the game world
+#
+# Similar to `gamnit::Sprite` which is in 2D.
+#
+# Each actor associates a `model` to the position `center`.
+# The appearance is modified by `rotation`, `scale` and `alpha`,
+# as well as the attributes of `model` itself.
+#
+# ~~~
+# import gamnit::depth
+#
+# # Load model from the assets folder
+# var model = new Model("path/in/assets.obj")
+#
+# # Create and configure an actor
+# var actor = new Actor(model, new Point3d[Float](0.0, 0.0, 0.0))
+# actor.scale = 2.0
+#
+# # Add to the visible game world
+# app.actors.add actor
+# ~~~
 class Actor
 
-       # Model used to dray this actor
+       # Model used to draw this actor
        var model: Model
 
        # Position of this sprite in world coordinates
@@ -29,14 +49,35 @@ class Actor
        # Rotation on the Z axis
        var rotation = 0.0 is writable
 
-       # Scale applied to this sprite
+       # Scale applied to the model
        var scale = 1.0 is writable
 
-       # Transparency applied to the texture on draw
+       # Transparency applied to the model on draw
+       #
+       # This value may be ignored by some materials.
+       # Non-opaque values may result in artifacts as there is no specialized
+       # support for transparent models and the depth buffer.
        var alpha = 1.0 is writable
 end
 
-# Entire 3D model defined by its `leaves`, an association of `Mesh` to `Material`
+# 3D model composed of `Mesh` and `Material`, loaded from the assets folder by default
+#
+# Instances can be created at any time and must be loaded after or at the end of `on_create`.
+# If loading fails, the model is replaced by `placeholder_model`.
+#
+# ~~~
+# import gamnit::depth
+#
+# var model = new Model("path/in/assets.obj")
+# model.load
+# ~~~
+#
+# The most simple model is `LeafModel`, composed of a single `Mesh` and `Material`.
+# It can be easily created programmatically to display simple geometries.
+# Whereas `CompositeModel` is composed of one or many `LeafModel` and is usually
+# loaded from the assets folder as a `ModelAsset`.
+# Instances of `ModelAsset` must be in the format OBJ and MAT,
+# and their texture in PNG or JPG.
 abstract class Model
 
        # Load this model in memory
@@ -56,7 +97,7 @@ class CompositeModel
        redef var leaves = new Array[LeafModel]
 end
 
-# Single model with a `mesh` and `material`
+# Basic model with a single `mesh` and `material`
 #
 # Only leaves are actually drawn by the `material`.
 class LeafModel
@@ -71,10 +112,28 @@ class LeafModel
        redef var leaves = [self]
 end
 
-# Material for a model or how to draw the model
+# Material for models, or how to draw the model
+#
+# To create a simple basic blueish material, use `new Material`.
+#
+# Each class of material is associated to a `GLProgram` and its GPU shaders.
+# The simple material `SmoothMaterial` allows to set an ambient, diffuse and specular color.
+# To which `TextureMaterial` adds three textures, for each kind of light.
+# The `NormalsMaterial` may be useful for debugging, it show the orientation of
+# the normal vectors as colors.
+#
+# ~~~
+# import gamnit::depth
+#
+# var blueish_material = new Material
+# var redish_material = new SmoothMaterial([0.3, 0.0, 0.0],
+#                                          [0.6, 0.0, 0.0],
+#                                          [1.0, 1.0, 1.0])
+# var normals_material = new NormalsMaterial
+# ~~~
 abstract class Material
 
-       # Draw `actor`
+       # Draw a `model` from `actor`
        #
        # This method should be refined by subclasses as the default implementation is a no-op.
        #
@@ -85,6 +144,17 @@ abstract class Material
 end
 
 # Mesh with all geometry data
+#
+# May be created via `Plane`, `Cube` or `UVSphere`,
+# or loaded from the assets folder indirectly with a `Model`.
+#
+# ~~~
+# import gamnit::depth
+#
+# var plane = new Plane
+# var cube = new Cube
+# var sphere = new UVSphere(1.0, 32, 16)
+# ~~~
 class Mesh
 
        # Vertices coordinates
@@ -105,58 +175,6 @@ class Mesh
 
        # `GLDrawMode` used to display this mesh, defaults to `gl_TRIANGLES`
        fun draw_mode: GLDrawMode do return gl_TRIANGLES
-
-       # Create an UV sphere of `radius` with `n_meridians` and `n_parallels`
-       init uv_sphere(radius: Float, n_meridians, n_parallels: Int)
-       do
-               var w = n_meridians
-               var h = n_parallels
-
-               var vertices = new Array[Float].with_capacity(w*h*3)
-               self.vertices = vertices
-
-               var texture_coords = new Array[Float].with_capacity(w*h*2)
-               self.texture_coords = texture_coords
-
-               var normals = new Array[Float].with_capacity(w*h*3)
-               self.normals = normals
-
-               # Build vertices
-               for m in [0..w[ do
-                       for p in [0..h[ do
-                               var u = m.to_f * 2.0 * pi / (w-1).to_f
-                               var v = p.to_f * pi / (h-1).to_f
-
-                               vertices.add radius * u.cos * v.sin
-                               vertices.add radius * v.cos
-                               vertices.add radius * u.sin * v.sin
-
-                               texture_coords.add (1.0 - m.to_f/(w-1).to_f)
-                               texture_coords.add(p.to_f/(h-1).to_f)
-
-                               normals.add u.cos * v.sin
-                               normals.add v.cos
-                               normals.add u.sin * v.sin
-                       end
-               end
-
-               # Build faces
-               var indices = new Array[Int].with_capacity((w-1)*(h-1)*6)
-               self.indices = indices
-               for m in [0..w-1[ do
-                       for p in [0..h-1[ do
-                               var a = m*h + p
-
-                               indices.add a
-                               indices.add a+h
-                               indices.add a+1
-
-                               indices.add a+h
-                               indices.add a+h+1
-                               indices.add a+1
-                       end
-               end
-       end
 end
 
 # Source of light
index b77c85e..4cc8dcb 100644 (file)
@@ -18,15 +18,17 @@ module more_materials
 intrude import depth_core
 intrude import flat
 
-# Simple material with static colors used for debugging or display abstract objects
-class SmoothMaterial
-       super Material
-
+redef class Material
        # Get the default blueish material
-       init default do init(
+       new do return new SmoothMaterial(
                [0.0, 0.0, 0.3, 1.0],
                [0.0, 0.0, 0.6, 1.0],
                [1.0, 1.0, 1.0, 1.0])
+end
+
+# Simple material with static colors
+class SmoothMaterial
+       super Material
 
        # Ambient color, always visible
        var ambient_color: Array[Float] is writable
@@ -144,7 +146,7 @@ class TexturedMaterial
                # If using a texture, set `texture_coords`
                program.tex_coord.array_enabled = sample_used_texture != null
                if sample_used_texture != null then
-                       if sample_used_texture isa GamnitRootTexture then
+                       if sample_used_texture isa RootTexture then
                                # Coordinates are directly valid
                                program.tex_coord.array(mesh.texture_coords, 2)
                        else
index eeeb258..0a4d53c 100644 (file)
@@ -80,6 +80,8 @@ class Plane
 end
 
 # Cube, with 6 faces
+#
+# Occupies `[-0.5..0.5]` on all three axes.
 class Cube
        super Mesh
 
@@ -132,3 +134,68 @@ class Cube
 
        redef var center = new Point3d[Float](0.0, 0.0, 0.0) is lazy
 end
+
+# Sphere with `radius` and a number of faces set by `n_meridians` and `n_parallels`
+class UVSphere
+       super Mesh
+
+       # Distance between the center and the vertices
+       var radius: Float
+
+       # Number of vertices on a full circle around the Z axis
+       var n_meridians: Int
+
+       # Number of vertices on an arc between both poles
+       var n_parallels: Int
+
+       init
+       do
+               var w = n_meridians
+               var h = n_parallels
+
+               var vertices = new Array[Float].with_capacity(w*h*3)
+               self.vertices = vertices
+
+               var texture_coords = new Array[Float].with_capacity(w*h*2)
+               self.texture_coords = texture_coords
+
+               var normals = new Array[Float].with_capacity(w*h*3)
+               self.normals = normals
+
+               # Build vertices
+               for m in [0..w[ do
+                       for p in [0..h[ do
+                               var u = m.to_f * 2.0 * pi / (w-1).to_f
+                               var v = p.to_f * pi / (h-1).to_f
+
+                               vertices.add radius * u.cos * v.sin
+                               vertices.add radius * v.cos
+                               vertices.add radius * u.sin * v.sin
+
+                               texture_coords.add (1.0 - m.to_f/(w-1).to_f)
+                               texture_coords.add(p.to_f/(h-1).to_f)
+
+                               normals.add u.cos * v.sin
+                               normals.add v.cos
+                               normals.add u.sin * v.sin
+                       end
+               end
+
+               # Build faces
+               var indices = new Array[Int].with_capacity((w-1)*(h-1)*6)
+               self.indices = indices
+               for m in [0..w-1[ do
+                       for p in [0..h-1[ do
+                               var a = m*h + p
+
+                               indices.add a
+                               indices.add a+h
+                               indices.add a+1
+
+                               indices.add a+h
+                               indices.add a+h+1
+                               indices.add a+1
+                       end
+               end
+       end
+end
index c2b9664..8d7d8a1 100644 (file)
@@ -62,7 +62,7 @@ class ModelAsset
                var content = text_asset.to_s
                if content.is_empty then
                        print_error "Model failed to load: Asset empty at '{self.path}'"
-                       leaves.add new LeafModel(new Cube, new SmoothMaterial.default)
+                       leaves.add new LeafModel(new Cube, new Material)
                        return
                end
 
@@ -71,7 +71,7 @@ class ModelAsset
                var obj_def = parser.parse
                if obj_def == null then
                        print_error "Model failed to load: .obj format error on '{self.path}'"
-                       leaves.add new LeafModel(new Cube, new SmoothMaterial.default)
+                       leaves.add new LeafModel(new Cube, new Material)
                        return
                end
 
@@ -212,7 +212,7 @@ private class ModelFromObj
                # Create models and store them
                for mesh, mtl_def in mesh_to_mtl do
                        var material = materials.get_or_null(mtl_def)
-                       if material == null then material = new SmoothMaterial.default
+                       if material == null then material = new Material
 
                        var model = new LeafModel(mesh, material)
                        array.add model
@@ -387,5 +387,5 @@ redef class Sys
        # Blue cube of 1 unit on each side, acting as placeholder for models failing to load
        #
        # This model can be freely used by any `Actor` as placeholder or for debugging.
-       var placeholder_model = new LeafModel(new Cube, new SmoothMaterial.default) is lazy
+       var placeholder_model = new LeafModel(new Cube, new Material) is lazy
 end
index 18744ee..3d0e841 100644 (file)
@@ -210,7 +210,7 @@ redef class TexturedMaterial
                # If using a texture, set `texture_coords`
                program.tex_coord.array_enabled = sample_used_texture != null
                if sample_used_texture != null then
-                       if sample_used_texture isa GamnitRootTexture then
+                       if sample_used_texture isa RootTexture then
                                # Coordinates are directly valid
                                program.tex_coord.array(mesh.texture_coords, 2)
                        else
index aef21d8..cbcc2e7 100644 (file)
@@ -46,11 +46,53 @@ import gamnit::dynamic_resolution
 import gamnit::limit_fps
 import gamnit::camera_control
 
-# Draw a `texture` at `center`
+# Visible 2D entity in the game world or UI
 #
-# An instance of `Sprite` can only belong to a single `SpriteSet` at
-# a time. The on screen position depends on the camera associated
+# Similar to `gamnit::Actor` which is in 3D.
+#
+# Each sprite associates a `texture` to the position `center`.
+# The appearance is modified by `rotation`, `invert_x`,
+# `scale`, `red`, `green`, `blue` and `alpha`.
+# These values can be changed at any time and will trigger an update
+# of the data on the GPU side, having a small performance cost.
+#
+# For a sprite to be visible, it must be added to either the world `sprites`
+# or the `ui_sprites`.
+# However, an instance of `Sprite` can only belong to a single `SpriteSet`
+# at a time. The final on-screen position depends on the camera associated
 # to the `SpriteSet`.
+#
+# ~~~
+# # Load texture and create sprite
+# var texture = new Texture("path/in/assets.png")
+# var sprite = new Sprite(texture, new Point3d[Float](0.0, 0.0, 0.0))
+#
+# # Add sprite to the visible game world
+# app.sprites.add sprite
+#
+# # Extra configuration of the sprite
+# sprite.rotation = pi/2.0
+# sprite.scale = 2.0
+#
+# # Show only the blue colors
+# sprite.red = 0.0
+# sprite.green = 0.0
+# ~~~
+#
+# To add a sprite to the UI it can be anchored to screen borders
+# with `ui_camera.top_left` and the likes.
+#
+# ~~~nitish
+# # Place it a bit off the top left of the screen
+# var pos = app.ui_camera.top_left.offset(128.0, -128.0, 0)
+#
+# # Load texture and create sprite
+# var texture = new Texture("path/in/assets.png")
+# var sprite = new Sprite(texture, pos)
+#
+# # Add it to the UI (above world sprites)
+# app.ui_sprites.add sprite
+# ~~~
 class Sprite
 
        # Texture drawn to screen
@@ -80,10 +122,10 @@ class Sprite
                center_direct = value
        end
 
-       # Rotation on the Z axis, positive values go counterclockwise
+       # Rotation on the Z axis, positive values turn counterclockwise
        var rotation = 0.0 is writable(rotation_direct=)
 
-       # Rotation on the Z axis, positive values go counterclockwise
+       # Rotation on the Z axis, positive values turn counterclockwise
        fun rotation=(value: Float)
        do
                if isset _rotation and value != rotation then needs_update
@@ -102,10 +144,12 @@ class Sprite
 
        # Scale applied to this sprite
        #
-       # The default size of `self` depends on the size in pixels of `texture`.
+       # The basic size of `self` depends on the size in pixels of `texture`.
        var scale = 1.0 is writable(scale_direct=)
 
-       # Scale applied to this sprite, see `scale`
+       # Scale applied to this sprite
+       #
+       # The basic size of `self` depends on the size in pixels of `texture`.
        fun scale=(value: Float)
        do
                if isset _scale and value != scale then needs_update
@@ -233,10 +277,10 @@ redef class App
        # Camera for `ui_sprites` using an orthogonal view
        var ui_camera = new UICamera(app.display.as(not null)) is lazy
 
-       # World sprites to draw as seen by `world_camera`
+       # World sprites drawn as seen by `world_camera`
        var sprites: Set[Sprite] = new SpriteSet
 
-       # UI sprites to draw as seen by `ui_camera`, drawn over world `sprites`
+       # UI sprites drawn as seen by `ui_camera`, over world `sprites`
        var ui_sprites: Set[Sprite] = new SpriteSet
 
        # Main method to refine in clients to update game logic and `sprites`
@@ -602,7 +646,7 @@ private class SpriteSet
        super HashSet[Sprite]
 
        # Map texture then static vs dynamic to a `SpriteContext`
-       var contexts_map = new HashMap2[GamnitRootTexture, Bool, SpriteContext]
+       var contexts_map = new HashMap2[RootTexture, Bool, SpriteContext]
 
        # Contexts in `contexts_map`
        var contexts_items = new Array[SpriteContext]
@@ -697,7 +741,7 @@ private class SpriteContext
        # Context config and state
 
        # Only root texture drawn by this context
-       var texture: nullable GamnitRootTexture
+       var texture: nullable RootTexture
 
        # OpenGL ES usage of `buffer_array` and `buffer_element`
        var usage: GLBufferUsage
@@ -1040,6 +1084,8 @@ end
 # The data can be compressed by a call to `defragment`.
 #
 # ~~~
+# intrude import gamnit::flat
+#
 # var array = new GroupedArray[String]
 # assert array.to_s == ""
 #
@@ -1199,6 +1245,8 @@ private class GroupedArray[E]
        # Returns the elements that moved as a list.
        #
        # ~~~
+       # intrude import gamnit::flat
+       #
        # var array = new GroupedArray[String]
        # array.add "a"
        # array.add "b"
diff --git a/lib/gamnit/font.nit b/lib/gamnit/font.nit
new file mode 100644 (file)
index 0000000..8a5defe
--- /dev/null
@@ -0,0 +1,82 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Abstract font drawing services, implemented by `bmfont` and `tileset`
+module font
+
+import flat
+
+# Abstract font, drawn by a `TextSprites`
+abstract class Font
+
+       # Line spacing modifier for `pld` and `plu`
+       #
+       # This value acts as multiplier to the standard line height.
+       # Defaults to 0.4, so a `pld` moves chars down by about half a line.
+       var partial_line_mod: Numeric = 0.4 is writable
+
+       # Backend writing service, clients should use `TextSprites.text=`
+       protected fun write_into(text_sprites: TextSprites, text: Text) is abstract
+end
+
+# Manage a set of sprites to display some text
+class TextSprites
+
+       # Font used to draw text
+       var font: Font
+
+       # Top left of the first character in UI coordinates
+       var anchor: Point3d[Float]
+
+       # Last set of sprites generated to display `text=`
+       var sprites = new Array[Sprite]
+
+       # Sprite set where to put created sprites
+       #
+       # Defaults to `app::ui_sprites`, but it could also be set to a
+       # `app::sprites` or a custom collection.
+       var target_sprite_set: Set[Sprite] = app.ui_sprites is lazy, writable
+
+       private var cached_text: nullable Text = ""
+
+       # Last text drawn
+       fun text: nullable Text do return cached_text
+
+       # Update the text displayed by inserting new sprites into `app.ui_sprites`
+       #
+       # Does not redraw if `text` has not changed.
+       fun text=(text: nullable Text)
+       is autoinit do
+               # Don't redraw if text hasn't changed
+               if text == cached_text then return
+               cached_text = text
+
+               # Clean up last used sprites
+               for s in sprites do if target_sprite_set.has(s) then target_sprite_set.remove s
+               sprites.clear
+
+               if text == null then return
+
+               font.write_into(self, text)
+
+               # Register sprites to be drawn by `app.ui_camera`
+               target_sprite_set.add_all sprites
+       end
+end
+
+# Partial line forward (U+008B)
+fun pld: Char do return '\8b'
+
+# Partial line backward (U+008C)
+fun plu: Char do return '\8c'
index 9b980fb..6b47788 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Services to load textures, create subtextures and manage their life-cycle
+# Load textures, create subtextures and manage their life-cycle
 module textures
 
 import display
 
-# Abstract gamnit texture
+# Texture composed of pixels, loaded from the assets folder by default
+#
+# Most textures should be created with `App` (as attributes)
+# for the method `on_create` to load them.
+#
+# ~~~
+# import gamnit::flat
+#
+# redef class App
+#     # Create the texture object, it will be loaded automatically
+#     var texture = new Texture("path/in/assets.png")
+#
+#     redef fun on_create()
+#     do
+#         # Let `on_create` load the texture
+#         super
+#
+#         # Use the texture
+#         var sprite = new Sprite(texture, new Point3d[Float](0.0, 0.0, 0.0))
+#         app.sprites.add sprite
+#     end
+# end
+# ~~~
+#
+# Otherwise, they can be loaded and error checked explicitly after `on_create`.
+#
+# ~~~nitish
+# var texture = new Texture("path/in/assets.png")
+# texture.load
+# var error = texture.error
+# if error != null then print_error error
+# ~~~
+#
+# A texture may also be created programmatically, like `CheckerTexture`,
+# or derived from another texture, using `subtexture`.
+# Textures with actual pixel data (not `Subtexture`) are `RootTexture`.
+# Texture loaded from the assets folder may in the PNG or JPG formats.
 abstract class Texture
 
        # Prepare a texture located at `path` within the `assets` folder
        new (path: Text) do return new TextureAsset(path.to_s)
 
-       # Root texture of which `self` is derived
-       fun root: GamnitRootTexture is abstract
+       # Root texture from which `self` is derived
+       fun root: RootTexture is abstract
 
        # Width in pixels of this texture
        var width = 0.0
@@ -42,10 +78,10 @@ abstract class Texture
        fun gl_texture: Int do return root.gl_texture
 
        # Prepare a subtexture from this texture, from the given pixel offsets
-       fun subtexture(left, top, width, height: Numeric): GamnitSubtexture
+       fun subtexture(left, top, width, height: Numeric): Subtexture
        do
                # Setup the subtexture
-               var subtex = new GamnitSubtexture(root, self, left.to_f, top.to_f, width.to_f, height.to_f)
+               var subtex = new Subtexture(root, self, left.to_f, top.to_f, width.to_f, height.to_f)
                return subtex
        end
 
@@ -64,7 +100,7 @@ end
 
 # Colorful small texture of 2x2 pixels
 class CheckerTexture
-       super GamnitRootTexture
+       super RootTexture
 
        redef fun load(force)
        do
@@ -84,7 +120,7 @@ class CheckerTexture
 end
 
 # Texture with its own pixels
-class GamnitRootTexture
+class RootTexture
        super Texture
 
        redef fun root do return self
@@ -143,7 +179,7 @@ end
 
 # Texture loaded from the assets folder
 class TextureAsset
-       super GamnitRootTexture
+       super RootTexture
 
        # Path to this texture within the `assets` folder
        var path: String
@@ -166,7 +202,7 @@ class TextureAsset
 end
 
 # Texture derived from another texture, does not own its pixels
-class GamnitSubtexture
+class Subtexture
        super Texture
 
        redef var root
index ecb40a2..1a784ae 100644 (file)
@@ -15,7 +15,7 @@
 # Support for `TileSet`, `TileSetFont` and drawing text with `TextSprites`
 module tileset
 
-import flat
+import font
 
 # Efficiently retrieve tiles in a big texture
 class TileSet
@@ -60,6 +60,7 @@ end
 # A monospace bitmap font where glyphs are stored in a tileset
 class TileSetFont
        super TileSet
+       super Font
 
        # Set the characters present in `texture`
        #
@@ -83,12 +84,6 @@ class TileSetFont
        # A negative value may display overlapped tiles.
        var vspace: Numeric = 0.0 is writable
 
-       # Line spacing modifier for `pld` and `plu`
-       #
-       # This value acts as multiplier to `height + vspace`.
-       # Defaults to 0.4, so a `pld` moves chars down by about half a line.
-       var partial_line_mod: Numeric = 0.4 is writable
-
        # The glyph/tile/texture associated to `char`
        #
        # Returns null if `char` is not in `chars`.
@@ -122,83 +117,37 @@ class TileSetFont
                var n_lines = text.chars.count('\n')
                return (n_lines+1).mul(height.add(vspace)).sub(vspace)
        end
-end
-
-# Manage a set of sprites to display some text
-class TextSprites
-
-       # Font used to draw text
-       var font: TileSetFont
-
-       # Top left of the first character in UI coordinates
-       var anchor: Point3d[Float]
-
-       # Last set of sprites generated to display `text=`
-       var sprites = new Array[Sprite]
-
-       # Sprite set where to put created sprites
-       #
-       # Defaults to `app::ui_sprites`, but it could also be set to a
-       # `app::sprites` or a custom collection.
-       var target_sprite_set: Set[Sprite] = app.ui_sprites is lazy, writable
-
-       private var cached_text: nullable Text = ""
-
-       # Last text drawn
-       fun text: nullable Text do return cached_text
-
-       # Update the text displayed by inserting new sprites into `app.ui_sprites`
-       #
-       # Does not redraw if `text` has not changed.
-       fun text=(text: nullable Text)
-       is autoinit do
-               # Don't redraw if text hasn't changed
-               if text == cached_text then return
-               cached_text = text
-
-               # Clean up last used sprites
-               for s in sprites do if target_sprite_set.has(s) then target_sprite_set.remove s
-               sprites.clear
-
-               if text == null then return
 
+       redef fun write_into(text_sprites, text)
+       do
                # Build new sprites
-               var dx = font.advance/2.0
-               var dy = font.hspace.to_f/2.0
+               var dx = advance/2.0
+               var dy = hspace.to_f/2.0
                for c in text do
                        if c == '\n' then
-                               dy -= font.height.to_f + font.vspace.to_f
-                               dx = font.advance/2.0
+                               dy -= height.to_f + vspace.to_f
+                               dx = advance/2.0
                                continue
                        else if c == pld then
-                               dy -= (font.height.to_f + font.vspace.to_f) * font.partial_line_mod.to_f
+                               dy -= (height.to_f + vspace.to_f) * partial_line_mod.to_f
                                continue
                        else if c == plu then
-                               dy += (font.height.to_f + font.vspace.to_f) * font.partial_line_mod.to_f
+                               dy += (height.to_f + vspace.to_f) * partial_line_mod.to_f
                                continue
                        else if c.is_whitespace then
-                               dx += font.advance
+                               dx += advance
                                continue
                        end
 
-                       var tex = font.char(c)
+                       var tex = char(c)
                        if tex == null then
                                # Try to fallback to '?'
-                               tex = font.char('?')
+                               tex = char('?')
                                if tex == null then continue
                        end
 
-                       sprites.add new Sprite(tex, anchor.offset(dx, dy, 0))
-                       dx += font.advance
+                       text_sprites.sprites.add new Sprite(tex, text_sprites.anchor.offset(dx, dy, 0))
+                       dx += advance
                end
-
-               # Register sprites to be drawn by `app.ui_camera`
-               target_sprite_set.add_all sprites
        end
 end
-
-# Partial line forward (U+008B)
-fun pld: Char do return '\8b'
-
-# Partial line backward (U+008C)
-fun plu: Char do return '\8c'
index cf5d0f8..3dc158a 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Provides interfaces and classes to represent basic geometry needs.
+# Interfaces and classes to represent basic geometry needs.
 module points_and_lines is serialize
 
 import serialization
 
-# An abstract 2d point, strongly linked to its implementation `Point`
+# Abstract 2d point, strongly linked to its implementation `Point`
 interface IPoint[N: Numeric]
 
        # Horizontal coordinate
@@ -84,7 +84,7 @@ interface IPoint[N: Numeric]
        redef fun ==(o) do return o isa IPoint[Numeric] and o.x == x and o.y == y
 end
 
-# A 2d point and an implementation of `IPoint`
+# 2D point with `x` and `z`
 class Point[N: Numeric]
        super IPoint[N]
 
@@ -92,17 +92,17 @@ class Point[N: Numeric]
        redef var y: N is writable
 end
 
-# An abstract 3d point, strongly linked to its implementation `Point3d`
+# Abstract 3d point, strongly linked to its implementation `Point3d`
 interface IPoint3d[N: Numeric]
        super IPoint[N]
 
-       # depth coordinate
+       # Depth coordinate
        fun z: N is abstract
 
        redef fun to_s do return "({x}, {y}, {z})"
 end
 
-# A 3d point and an implementation of `IPoint3d`
+# 3D point with `x`, `y` and `z`
 class Point3d[N: Numeric]
        super IPoint3d[N]
        super Point[N]
@@ -110,21 +110,21 @@ class Point3d[N: Numeric]
        redef var z: N is writable
 end
 
-# An abstract 2d line segment
+# Abstract 2D line segment between two ordered points
 interface ILine[N: Numeric]
        # The type of points that ends the segment
        type P: IPoint[N]
 
-       # The point that is the left-end of the segment
+       # Point at the left-end of the segment
        fun point_left: P is abstract
 
-       # The point that is the right-end of the segment
+       # Point at the right-end of the segment
        fun point_right: P is abstract
 
        redef fun to_s do return "{point_left}--{point_right}"
 end
 
-# A 2d line segment
+# 2D line segment between two ordered points
 class Line[N: Numeric]
        super ILine[N]
 
@@ -142,14 +142,14 @@ class Line[N: Numeric]
        end
 end
 
-# An abstract 3d line segment
+# Abstract 3D line segment between two ordered points
 interface ILine3d[N: Numeric]
        super ILine[N]
 
        redef type P: IPoint3d[N]
 end
 
-# A 3d line segment
+# 3D line segment between two ordered points
 class Line3d[N: Numeric]
        super Line[N]
        super ILine3d[N]
index 3062d5e..81688d9 100644 (file)
@@ -35,7 +35,20 @@ class MultiHashMap[K, V]
        super HashMap[K, Array[V]]
 
        # Add `v` to the array associated with `k`.
+       #
        # If there is no array associated, then create it.
+       #
+       # For the inverse operation, see `remove_one`.
+       #
+       # ```
+       # var m = new MultiHashMap[String, Char]
+       # m.add_one("four", 'i')
+       # m.add_one("four", 'i')
+       # m.add_one("four", 'i')
+       # m.add_one("four", 'i')
+       # assert m.has_key("four")
+       # assert m["four"] == ['i', 'i', 'i', 'i']
+       # ```
        fun add_one(k: K, v: V)
        do
                var x = self.get_or_null(k)
@@ -51,6 +64,44 @@ class MultiHashMap[K, V]
                self[key] = res
                return res
        end
+
+       # Remove an occurrence of `v` from the array associated with `k`.
+       #
+       # If the associated array does not contain `v`, do nothing. If the
+       # associated array only contain one element and this element is `v`, remove
+       # the key `k`.
+       #
+       # In a nutshell, does the inverse operation of `add_one`.
+       #
+       # ```
+       # var m = new MultiHashMap[String, Char]
+       # m["four"]        =  ['4', 'i', 'i', 'i', 'i']
+       # m.remove_one("four", 'i')
+       # assert m["four"] == ['4', 'i', 'i', 'i']
+       #
+       # m = new MultiHashMap[String, Char]
+       # m.add_one("one", '1')
+       # m.remove_one("one", '?')
+       # assert m["one"] == ['1']
+       # m.remove_one("one", '1')
+       # assert not m.has_key("one")
+       # assert m["one"] == new Array[Char]
+       #
+       # m = new MultiHashMap[String, Char]
+       # m.add_one("one", '1')
+       # m.remove_one("two", '2')
+       # assert not m.has_key("two")
+       # assert m["one"] == ['1']
+       # assert m["two"] == new Array[Char]
+       # ```
+       fun remove_one(k: K, v: V)
+       do
+               var x = get_or_null(k)
+               if x != null then
+                       x.remove(v)
+                       if x.is_empty then keys.remove(k)
+               end
+       end
 end
 
 # Simple way to store an `HashMap[K1, HashMap[K2, V]]`
index c93d39a..a7789f8 100644 (file)
@@ -127,7 +127,7 @@ end
 # A count option. Count the number of time this option is present
 class OptionCount
        super Option
-       redef type VALUE: Int
+       redef type VALUE: Int is fixed
 
        # Init a new OptionCount with a `help` message and `names`.
        init(help: String, names: String...) is old_style_init do super(help, 0, names)
diff --git a/lib/popcorn/pop_tasks.nit b/lib/popcorn/pop_tasks.nit
new file mode 100644 (file)
index 0000000..a0e1aa1
--- /dev/null
@@ -0,0 +1,78 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Popcorn threaded tasks
+#
+# Tasks allow you to execute code in another thread than the app listening loop.
+# Useful when you want to run some tasks periodically.
+#
+# Let's say you want to purge the `downloads/` directory of your app every hour:
+#
+# ~~~nitish
+# class PurgeTask
+#      super PopTask
+#
+#      var dir: String
+#
+#      redef fun main do
+#              loop
+#                      dir.rmdir
+#                      3600.sleep
+#              end
+#      end
+# end
+#
+# var app = new App
+#
+# # Register a new task
+# app.register_task(new PurgeTask("downloads/"))
+#
+# # Add your handlers
+# # app.use('/', new MyHandler)
+#
+# # Run the tasks
+# app.run_tasks
+#
+# # Start the app
+# app.listen("0.0.0.0", 3000)
+# ~~~
+module pop_tasks
+
+import pop_handlers
+import pthreads
+
+# An abstract Popcorn task
+#
+# Redefine the `main` method to do something
+#
+# TODO provide a CRON-like syntax like SpringBoot?
+abstract class PopTask
+       super Thread
+
+       redef fun main do return null
+end
+
+redef class App
+
+       # Tasks to run
+       var tasks = new Array[PopTask]
+
+       # Register a new task in `self`
+       fun register_task(task: PopTask) do tasks.add task
+
+       # Run all registered tasks
+       fun run_tasks do for task in tasks do task.start
+end
index 9c54b76..e95e517 100644 (file)
@@ -138,20 +138,15 @@ class ValidationResult
        # Does `self` contains `errors`?
        fun has_error: Bool do return errors.not_empty
 
-       # Render self as a JsonObject
-       fun json: JsonObject do
-               var obj = new JsonObject
-               obj["has_error"] = has_error
-               var e = new JsonObject
-               for k, v in errors do
-                       e[k] = new JsonArray.from(v)
-               end
-               obj["errors"] = e
-               return obj
+       redef fun core_serialize_to(v) do
+               var errors = new JsonObject
+               for k, e in self.errors do
+                       errors[k] = new JsonArray.from(e)
+               end
+               v.serialize_attribute("has_error", has_error)
+               v.serialize_attribute("errors", errors)
        end
 
-       redef fun serialize_to(v) do json.serialize_to(v)
-
        # Returns the validation result as a pretty formated string
        fun to_pretty_string: String do
                var b = new Buffer
@@ -699,3 +694,70 @@ class ISBNField
 
        redef var re = "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re
 end
+
+# Check if a field is a valid URL
+#
+# Matched against the following regular expression:
+# ~~~raw
+# ^(http|https):\/\/[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)+([a-zA-Z0-9\-\.,@?^=%&amp;:/~\+#]*[a-zA-Z0-9\-\@?^=%&amp;/~\+#])?
+# ~~~
+# You should redefine the base regular expression `re` with your own.
+#
+# ~~~
+# var validator = new ObjectValidator
+# validator.add new URLField("url")
+# assert not validator.validate("""{ "url": "" }""")
+# assert not validator.validate("""{ "url": "foo" }""")
+# assert not validator.validate("""{ "url": "http://foo" }""")
+# assert validator.validate("""{ "url": "http://nitlanguage.org" }""")
+# assert validator.validate("""{ "url": "http://nitlanguage.org/foo" }""")
+# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q" }""")
+# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a" }""")
+# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a=1" }""")
+# ~~~
+class URLField
+       super RegexField
+
+       autoinit field, required
+
+       redef var re = "^(http|https):\\/\\/[a-zA-Z0-9\\-_]+(\\.[a-zA-Z0-9\\-_]+)+([a-zA-Z0-9\\-\\.,@?^=%&:/~\\+#]*[a-zA-Z0-9\\-\\@?^=%&/~\\+#])?".to_re
+end
+
+# Check if a field value is already used
+#
+# This class provides a stub validator for fields that should contain a unique value along an
+# application (typically logins or ids).
+#
+# Here an example that uses a `Repository` if an email is unique:
+# ~~~nitish
+# class UniqueEmailField
+#      super UniqueField
+#
+#      var users: UsersRepository
+#
+#      redef fun check_unicity(v, field, val) do
+#              var user = users.find_by_email(val)
+#              if user != null then
+#                      v.validation.add_error(field, "Email `{val}` already used")
+#                      return false
+#              end
+#              return true
+#      end
+# end
+# ~~~
+class UniqueField
+       super StringField
+
+       # Check if `val` is already used somewhere
+       #
+       # You must redefine this method to handle your own validation.
+       fun check_unicity(v: ObjectValidator, field, val: String): Bool is abstract
+
+       redef fun validate_field(v, obj) do
+               if not super then return false
+               var val = obj.get_or_null(field)
+               if not val isa String then return false
+               if not check_unicity(v, field, val) then return false
+               return true
+       end
+end
index 190d312..0ae1017 100644 (file)
@@ -18,6 +18,7 @@
 module popcorn
 
 import nitcorn
+import pop_tasks
 import pop_sessions
 import pop_logging
 intrude import pop_handlers
index bff3243..80f29ec 100644 (file)
@@ -160,9 +160,16 @@ end
 #
 # Possible colors:
 #
-# * A:0, B:1, C: 2, D: 1, E: 3, F:3, G:2, H:4
+# * A: 0
+# * B: 1
+# * C: 2
+# * D: 1
+# * E: 3
+# * F: 3
+# * G: 2
+# * H: 4
 #
-# see: Ducournau, R. (2011).
+# SEE: Ducournau, R. (2011).
 # Coloring, a versatile technique for implementing object-oriented languages.
 # Software: Practice and Experience, 41(6), 627–659.
 class POSetColorer[E: Object]
@@ -171,6 +178,9 @@ class POSetColorer[E: Object]
        var is_colored = false
 
        # Resulting ids
+       #
+       # All ids are strictly positive (`>= 1`).
+       #
        # REQUIRE: is_colored
        fun ids: Map[E, Int] do
                assert is_colored
@@ -216,7 +226,7 @@ class POSetColorer[E: Object]
                ids_cache.clear
                var elements = new HashSet[E].from(poset_cache.to_a)
                for e in poset_cache.linearize(elements) do
-                       ids_cache[e] = ids_cache.length
+                       ids_cache[e] = ids_cache.length + 1
                end
        end
 
diff --git a/src/compiler/test_coloring.nit b/src/compiler/test_coloring.nit
new file mode 100644 (file)
index 0000000..867afcf
--- /dev/null
@@ -0,0 +1,32 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module test_coloring is test_suite
+
+import test_suite
+import coloring
+
+class TestPOSetColorer
+       super TestSuite
+
+       fun test_ids_strictly_positive do
+               var poset = new POSet[String]
+               poset.add_node "A"
+
+               var colorer = new POSetColorer[String]
+               colorer.colorize(poset)
+
+               assert colorer.ids["A"] > 0
+       end
+end
index a2fb201..216e2b3 100644 (file)
@@ -235,7 +235,7 @@ end
 class IntMetric
        super Metric
 
-       redef type VAL: Int
+       redef type VAL: Int is fixed
        redef type RES: Counter[ELM]
 
        # `IntMetric` uses a Counter to store values in intern.
index c1c950e..21b182e 100644 (file)
@@ -154,6 +154,14 @@ redef class MModule
        # (introduction and refinement)
        var mclassdefs = new Array[MClassDef]
 
+       private var mclassdef_sorter: MClassDefSorter is lazy do
+               return new MClassDefSorter(self)
+       end
+
+       private var mpropdef_sorter: MPropDefSorter is lazy do
+               return new MPropDefSorter(self)
+       end
+
        # Does the current module has a given class `mclass`?
        # Return true if the mmodule introduces, refines or imports a class.
        # Visibility is not considered.
@@ -169,7 +177,7 @@ redef class MModule
        # Visibility is not considered.
        #
        # Note: this function is expensive and is usually used for the main
-       # module of a program only. Do not use it to do you own subtype
+       # module of a program only. Do not use it to do your own subtype
        # functions.
        fun flatten_mclass_hierarchy: POSet[MClass]
        do
@@ -201,8 +209,7 @@ redef class MModule
        # The most general is first, the most specific is last
        fun linearize_mclassdefs(mclassdefs: Array[MClassDef])
        do
-               var sorter = new MClassDefSorter(self)
-               sorter.sort(mclassdefs)
+               mclassdef_sorter.sort(mclassdefs)
        end
 
        # Sort a given array of property definitions using the linearization order of the module
@@ -210,8 +217,7 @@ redef class MModule
        # The most general is first, the most specific is last
        fun linearize_mpropdefs(mpropdefs: Array[MPropDef])
        do
-               var sorter = new MPropDefSorter(self)
-               sorter.sort(mpropdefs)
+               mpropdef_sorter.sort(mpropdefs)
        end
 
        private var flatten_mclass_hierarchy_cache: nullable POSet[MClass] = null
@@ -355,21 +361,19 @@ private class MPropDefSorter
        super Comparator
        redef type COMPARED: MPropDef
        var mmodule: MModule
+
        redef fun compare(pa, pb)
        do
                var a = pa.mclassdef
                var b = pb.mclassdef
-               var ca = a.mclass
-               var cb = b.mclass
-               if ca != cb then return mmodule.flatten_mclass_hierarchy.compare(ca, cb)
-               return mmodule.model.mclassdef_hierarchy.compare(a, b)
+               return mmodule.mclassdef_sorter.compare(a, b)
        end
 end
 
 # A named class
 #
-# `MClass` are global to the model; it means that a `MClass` is not bound to a
-# specific `MModule`.
+# `MClass`es are global to the model; it means that a `MClass` is not bound
+# to a specific `MModule`.
 #
 # This characteristic helps the reasoning about classes in a program since a
 # single `MClass` object always denote the same class.
@@ -473,11 +477,13 @@ class MClass
        end
 
        # The kind of the class (interface, abstract class, etc.)
-       # In Nit, the kind of a class cannot evolve in refinements
+       #
+       # In Nit, the kind of a class cannot evolve in refinements.
        var kind: MClassKind
 
        # The visibility of the class
-       # In Nit, the visibility of a class cannot evolve in refinements
+       #
+       # In Nit, the visibility of a class cannot evolve in refinements.
        redef var visibility
 
        init
@@ -501,12 +507,12 @@ class MClass
        # Warning: such a definition may not exist in the early life of the object.
        # In this case, the method will abort.
        #
-       # Use `try_intro` instead
+       # Use `try_intro` instead.
        var intro: MClassDef is noinit
 
-       # The definition that introduces the class or null if not yet known.
+       # The definition that introduces the class or `null` if not yet known.
        #
-       # See `intro`
+       # SEE: `intro`
        fun try_intro: nullable MClassDef do
                if isset _intro then return _intro else return null
        end
@@ -1472,8 +1478,8 @@ class MVirtualType
 
        # A VT is fixed when:
        # * the VT is (re-)defined with the annotation `is fixed`
-       # * the VT is (indirectly) bound to an enum class (see `enum_kind`) since there is no subtype possible
-       # * the receiver is an enum class since there is no subtype possible
+       # * the receiver is an enum class since there is no subtype that can
+       #   redefine this virtual type
        redef fun lookup_fixed(mmodule: MModule, resolved_receiver: MType): MType
        do
                assert not resolved_receiver.need_anchor
@@ -1487,13 +1493,10 @@ class MVirtualType
                # Recursively lookup the fixed result
                res = res.lookup_fixed(mmodule, resolved_receiver)
 
-               # 1. For a fixed VT, return the resolved bound
+               # For a fixed VT, return the resolved bound
                if prop.is_fixed then return res
 
-               # 2. For a enum boud, return the bound
-               if res isa MClassType and res.mclass.kind == enum_kind then return res
-
-               # 3. for a enum receiver return the bound
+               # For a enum receiver return the bound
                if resolved_receiver.mclass.kind == enum_kind then return res
 
                return self
@@ -1614,9 +1617,7 @@ class MParameterType
        end
 
        # A PT is fixed when:
-       # * Its bound is a enum class (see `enum_kind`).
-       #   The PT is just useless, but it is still a case.
-       # * More usually, the `resolved_receiver` is a subclass of `self.mclass`,
+       # * The `resolved_receiver` is a subclass of `self.mclass`,
        #   so it is necessarily fixed in a `super` clause, either with a normal type
        #   or with another PT.
        #   See `resolve_for` for examples about related issues.
@@ -1635,13 +1636,7 @@ class MParameterType
                #print "{class_name}: {self}/{mtype}/{anchor}?"
 
                if mtype isa MGenericType and mtype.mclass == self.mclass then
-                       var res = mtype.arguments[self.rank]
-                       if anchor != null and res.need_anchor then
-                               # Maybe the result can be resolved more if are bound to a final class
-                               var r2 = res.anchor_to(mmodule, anchor)
-                               if r2 isa MClassType and r2.mclass.kind == enum_kind then return r2
-                       end
-                       return res
+                       return mtype.arguments[self.rank]
                end
 
                # self is a parameter type of mtype (or of a super-class of mtype)
@@ -2582,7 +2577,7 @@ class MClassKind
 
        # TODO: private init because enumeration.
 
-       # Can a class of kind `self` specializes a class of kine `other`?
+       # Can a class of kind `self` specializes a class of kind `other`?
        fun can_specialize(other: MClassKind): Bool
        do
                if other == interface_kind then return true # everybody can specialize interfaces
index d8dc9eb..bcbacb3 100644 (file)
@@ -1211,8 +1211,9 @@ class AIntrudeVisibility
 end
 
 # A class definition
-# While most definition are `AStdClassdef`
-# There is tow special case of class definition
+#
+# While most definitions are `AStdClassdef`s,
+# there are 2 special cases of class definitions.
 abstract class AClassdef
        super Prod
        # All the declared properties (including the main method)
index 0c08063..a6e4bef 100644 (file)
@@ -62,7 +62,7 @@ class RapidTypeAnalysis
        # live_methods to determine new methoddefs to visit
        var live_types = new HashSet[MClassType]
 
-       # The pool of undesolved live types
+       # The pool of unresolved live types
        # They are globally resolved at the end of the analaysis
        var live_open_types = new HashSet[MClassType]
 
index 7de400a..07ad951 100644 (file)
@@ -199,8 +199,7 @@ class VirtualMachine super NaiveInterpreter
        do
                if mclass.loaded then return
 
-               # Recursively load superclasses
-               for parent in mclass.in_hierarchy(mainmodule).direct_greaters do load_class_indirect(parent)
+               load_supers(mclass)
 
                if mclass.abstract_loaded then
                        mclass.allocate_vtable(self)
@@ -209,6 +208,14 @@ class VirtualMachine super NaiveInterpreter
                end
        end
 
+       # Recursively load superclasses.
+       private fun load_supers(mclass: MClass)
+       do
+               for parent in mclass.in_hierarchy(mainmodule).direct_greaters do
+                       load_class_indirect(parent)
+               end
+       end
+
        # This method is called to handle an implicitly loaded class,
        # i.e. a superclass of an explicitly loaded class
        # A class loaded implicitly will not be fully allocated
@@ -217,7 +224,7 @@ class VirtualMachine super NaiveInterpreter
                # It the class was already implicitly loaded
                if mclass.abstract_loaded then return
 
-               for parent in mclass.in_hierarchy(mainmodule).direct_greaters do load_class_indirect(parent)
+               load_supers(mclass)
 
                mclass.make_vt(self, false)
        end
index 6681b3d..002956f 100644 (file)
@@ -35,7 +35,7 @@ class B
        redef fun bar2: Int do return 33
 
        redef fun baz1 do return 44
-       #alt1#redef fun baz2: Int do return 55
+       #alt1#redef fun baz2 do return 55
 end
 
 fun test_a(a: A[Object])
index ae839ed..7f43b63 100644 (file)
@@ -17,7 +17,7 @@
 
 class BackIntComparator
        super Comparator
-       redef type COMPARED: Int
+       redef type COMPARED: Int is fixed
        redef fun compare(a: Int, b: Int): Int
        do
                return b <=> a
@@ -28,7 +28,7 @@ end
 
 class DecimalComparator
        super Comparator
-       redef type COMPARED: Int
+       redef type COMPARED: Int is fixed
        redef fun compare(a: Int, b: Int): Int
        do
                return (a%10) <=> (b%10)
index 3eeff31..27b5633 100644 (file)
@@ -1,3 +1,3 @@
-alt/base_virtual_type_self_alt2.nit:44,9: Type Error: expected `Int`, got `Float`.
-alt/base_virtual_type_self_alt2.nit:45,7--12: Type Error: expected `Float`, got `Int`.
-alt/base_virtual_type_self_alt2.nit:47,7--12: Type Error: expected `Float`, got `A[Int]`.
+alt/base_virtual_type_self_alt2.nit:44,9: Type Error: expected `U`, got `Float`.
+alt/base_virtual_type_self_alt2.nit:45,7--12: Type Error: expected `Float`, got `U: Int`.
+alt/base_virtual_type_self_alt2.nit:47,7--12: Type Error: expected `Float`, got `A[U]: A[Int]`.
diff --git a/tests/sav/integer_overflow.res b/tests/sav/integer_overflow.res
new file mode 100644 (file)
index 0000000..9f438eb
--- /dev/null
@@ -0,0 +1,10 @@
+Signed 32-bit:
+-2147483648
+-294967296
+2
+-2147479015
+unsigned 32-bit:
+1
+1705032704
+2147483648
+131073